archive by month
Skip to content

interesting Chobo-Steamhammer game

The SCHNAIL web site was updated as promised, and looks much prettier. On the leaderboard, “Download” is still spelled “Dowload” though.

Today is an interesting game from SCHNAIL, Chobo (P) vs Steamhammer on Python. Chobo played corsair-reaver with mass reaver drops to destroy bases and an eventual switch into carriers, a classic strategy I have never seen used against Steamhammer. It’s a demanding strategy for both players. Protoss must be fast and aggressive with drops, never leaving its expensive forces long at home because zerg grows back fast. And zerg must cope with the overwhelming splash damage of reavers on the ground and corsairs in the air—the units may not work against Monster, but they are deadly against Steamhammer. Classic zerg play against corsair-reaver includes burrowed zerglings around the map to see the reaver drops coming, a skill that Steamhammer does not have.

Corsair-reaver depends on a heavy force of tech units, so it launches slowly. Chobo curiously did not take the natural, but blocked the ramp with 2 zealots and a dragoon while teching, then took the nearby island base—which, by the way, I think Steamhammer never discovered. Steamhammer luckily started with a 3-hatch strategy before it scouted the protoss base, so it did not fall behind right off. A one-reaver probing drop did light damage at the zerg natural, then the reaver relocated to the morphing zerg third to try to prevent it. But scourge chased the shuttle away and hydra-ling chopped the reaver. Chobo learned a little caution.

Seeing the reavers, Steamhammer elected to make mutalisks despite the corsairs. It does understand the tradeoff risk... in a vague way. Soon protoss moved down the ramp to take the natural as a third base. By this point, Steamhammer was worried by the powerful-looking protoss force and went with army over economy, starting to fall behind. Chobo tried a 2-reaver drop at the zerg third and killed the defending sunken, but the drones burrowed and protoss left rather than risk the reavers. Still, with 2 bases and another coming versus 3 undersaturated zerg bases, protoss was ahead. Chobo dropped again, again killing the replaced sunken and again retreating. Seeing the drones instantly burrow and unburrow as the reaver appeared and disappeared must have been amusing.

protoss moves into the natural

At this point I think the human player began to go wrong. Chobo made defensive cannons and moved by air to take a 4th base, dedicating reavers and adding ramp cannons for its defense. Chobo was perhaps concerned about APM and reaction speed, limited resources if you’re human. But a human can’t outmacro Steamhammer without keeping pressure on. The reavers in speed shuttles are highly mobile. If protoss wants to take another base, I think it’s correct to airlift in minimal defense and rely on the main force to fly to the rescue in case of trouble. In case of no trouble, those reavers want to be blowing stuff up, or at least threatening to. Anyway, a zerg base was morphing below the new protoss base, and the hatchery did not last long. But Steamhammer, seeing more cannons and not seeing more army, correctly concluded that it could make drones and tech up. At the time of the picture, defiler consume is researching.

plus one protoss base, minus one zerg base

Steamhammer poked the protoss main with its mutalisks, and Chobo responded by chasing the mutas back to the zerg base and eradicating them, with minor corsair losses. Steamhammer does not understand that air units can scatter and keep fleeing, it believes they have to make a last stand to defend the base. Zerg was reduced to 15 army supply versus 52 army supply for protoss.

It didn’t matter. Zerg had the economy and soon reached its drone limit of 75 (this was Steamhammer 3.4.8, not 3.5 which has a limit of 65). When the corsairs and shuttles moved toward the zerg army, they were plagued and then ensnared, so that the outnumbered mutalisks (11 corsairs with attack +2, 10 mutas with carapace +2) held the upper hand. Protoss dropped the zerg third again, this time with mass reavers and disruption web, and eradicated it. With an observer, the reavers wiped the burrowed drones too. Steamhammer countered in the protoss natural with dark swarm and zerglings and returned the favor. In the picture, Steamhammer has realized its mineral excess and is adding hatcheries to burn it off—and meanwhile, the overloaded human can’t keep up with macro (watch the APM figures).

ensnare and plague, ouchies

Carriers arrived and there was more fighting, but the strategic position was decided. Drones and hive tech beat reavers and carriers. The picture is shortly after a base full of drones was destroyed, but as soon as the drones visible in the production tab finish, Steamhammer will have re-maxed its drone count.

dark swarm everywhere

I wish the game had lasted a little longer, so that I could see how well Steamhammer’s endgame scouting and clean-up of islands works. It theoretically has the skills, but so few opponents take island bases that they are untested in real games.

An entertaining game. A lesson for zerg bots: Defiler skills count! Steamhammer needed both plague and swarm to win. A lesson for human players: Keep the pressure on! There is one regular SCHNAIL opponent for Steamhammer who seems to enjoy playing tower defense as terran: Stay alive as long as possible with bunkers and tanks and turrets. The two have played dozens of times, and Steamhammer has won every game. Defending does not keep you alive, attacking keeps you alive.

Steamhammer 3.5 change list

Steamhammer 3.5 is uploaded to SCHNAIL, and will hit SSCAIT when SSCAIT comes back up. I need to collect more opening data before I can work with it seriously; it is taking longer than I expected. But I still made Steamhammer better.

I have been feeling lazy and haven’t gotten much done, according to me. I’m not sure you’ll be able to tell from this change list, but then again, I haven’t posted to the blog recently or done any of the SCHNAIL analysis that I still want to do. It’s in accordance with my motto: All things in due time, or later.

code

Updated to VS 2019. When I upgraded to BWAPI 4.4.0, I followed the instructions, which said to use VS 2017. I didn’t realize until later that I could go further. This update was worth another 5% reduction in DLL size thanks to better optimization.

• Many files, not all of them, are reformatted with spaces instead of the grotesque mix of spaces and tabs that I’ve been living with out of indolence. Sometimes I think that the inclusion of the TAB character in ASCII in the 1960s has cost more confusion and dismay than the entire Unicode character set has since.

squad orders

• The flying squad has a slight preference to attack the enemy main over other bases. The ground squad continues to prefer to attack the natural, other things being equal.

infrastructure

The ground attack map the.groundAttacks now includes enemy sieged tanks and burrowed lurkers; formerly, it only counted attacks from static defense buildings. The idea is that units which often sit in place should be included in pathfinding for safe paths and minimum-damage paths (which are not implemented yet), while those that move around should be handled by reactive means, at least for many purposes. In the meantime, the attack map does affect other decisions, such as building placement, which will now become smarter (“doh, don’t place a building where it will be in tank range”).

UnitInfo adds a powered flag to keep track of whether a protoss building had pylon power when last seen.

• Unpowered cannons are excluded from the combat sim, using the powered flag. Steamhammer used to be afraid of them, if they happened to be out of sight so that their powerlessness was not apparent.

defense reactions

• Declare that the workers at a base are in danger if the ground attack map says that the command center/nexus/hatchery is in enemy range. This is in response to SCHNAIL games where protoss did the proxy pylon-pylon-cannon trick behind the mineral line (the pylons and minerals block zerglings from the cannon). Steamhammer pulled its workers to safety, but they remained assigned to the base and were not transferred out (they danced behind the hatchery getting in the way). When the workers at a base are in danger, Steamhammer assigns them elsewhere when it can and assigns no new workers to the base until the danger has passed. It should react better now.

• Don’t assign a worker to mine any mineral patch which is in enemy range, according to the ground attack map. It’s common for an enemy proxy to reach part of the mineral line and not the rest. Now Steamhammer will know to abandon the mineral patches that are in danger, releasing workers for elsewhere. If worker self-defense is not triggered, it will continue mining the rest.

• Cancellation of doomed unfinished buildings and units in the egg or cocoon is improved. I originally added this long ago, and it used a simple hitpoint limit because there was no infrastructure for anything better (too low on HP, cancel it). Now it is updated to use ExpectedSurvivalTime(), which adds up the damage rates of the attackers. I expect it to be more accurate at canceling things at the last second.

zerg

AbsoluteMaxWorkers is configured to 65 instead of Steamhammer’s traditional 75. I want to see how big a difference it makes. In the middle game, Steamhammer always aims for its max worker count, which tends to release pressure on the opponent until the late game—watch games and you can’t miss it. It needs the ability to do things like stop drone production at a point where the income and hatchery count are ideal for a given unit mix, and pour on the army production. This change is an experiment to gather some data and help me think about when and how to do that.

Queens are more responsive. Among other points, a queen which is in the process of casting is controlled frame by frame rather than only once every 12 (!) frames. Queens will less often blunder into fire and more often escape after casting. Queens have proved effective in games versus terran with many tanks, so it’s important.

• Defilers are a little more responsive too. The change is smaller. Improving the use of dark swarm would be more important.... Maybe this summer?

If a unit is under dark swarm and no enemy can hit it, it won’t retreat but will keep fighting, even if comrades outside the swarm run away. Units still don’t have the sense to run into dark swarm when they should, but this is a step along the way.

• Fixed: Defilers could repeatedly plague cannons, doing it again as soon as the last plague wore off. They got a constant bonus for plaguing a static defense building. Now they get a proper variable bonus depending on how many HP they expect to wipe off. There is a separate fix to prevent over-plaguing of terran buildings which are already in the red and burning down. Let terran repair it a little first.

Add sunkens or spores in the opening to hold vultures or air attacks. Due to a bug inserted last December, these defensive reactions didn’t happen until Steamhammer got out of its opening book, which could be too late.

• Don’t make scourge at all versus mass corsairs or mass battlecruisers. They’ll shoot down almost all of it, at huge cost in gas and larvas. There are disadvantages to this decision, like loss of drop defense, and I’ll relax it when scourge gets smarter.

• Battlecruisers more strongly call for a hydralisk answer.

• Tanks more strongly contraindicate lurker production. I was still seeing some lurkers even versus mass tanks.

openings

However long it may take, opening selection using data is coming. When it arrives, the advantages of having many builds will increase and the disadvantages will fade. I felt free to add as many ideas as I could come up with.

• Removed 4Scout, which is functionally identical to 5Scout, and added 7Scout 8Scout 8Overscout. 5Scout and 6Scout remain. These are fast scouting builds which leave all decisions to the strategy boss. They can be appropriate when the opponent is unpredictable but tends to play into the strategy boss’s strengths—it does happen.

• Added new 7PoolHarder 7Pool10Hatch 8PoolHard rush builds. Can’t have too many different rushes.

• Updated 8Hatch7Pool 8Hatch7PoolSpeed 8Hatch7PoolBurrow 8Hatch7PoolBurrowB to be more efficient.

Added 9PoolFastLair 9PoolFastSpire 9PoolFastSpireB 9PoolFastLurker, which get the fastest possible lair after a 9 pool. The 9PoolFastSpire variant is more suitable versus zerg, and 9PoolFastSpireB versus protoss forge expand.

Added OverpoolFastLair ZvZ_OverpoolFastMuta, which get the fastest possible lair after an overpool.

• Added Overpool14Hatch, which seems like a good opening stem versus protoss. After the spawning pool it makes 4 drones (not 3) and 2 pairs of zerglings (not 3), then the second hatchery. With appropriate followup, which doesn’t happen yet, it should be fine versus either 1 base or 2 base protoss play. If the strategy boss knew how to adapt properly, I would have marked this as a key improvement.

• Added 11Gas10PoolMutaB 11Gas10PoolLurkerB. When I first added the 11 gas 10 pool build, way back in Steamhammer 1.0, I debated whether to make 2 drones after the spawning pool and before the lair, or 3. I settled on 2, because it seemed safer in ZvZ; the third drone delays zerglings slightly. These minor variants make 3 instead.

• Added 12-11HatchStem, a 3 hatch before pool build which stops after the spawning pool finishes, without making any lings. Steamhammer already has a couple other variants, with specific continuations.

more notes on SCHNAIL

A few unrelated things I’ve noticed on SCHNAIL.

bad bots

There are a couple of bad bots that don’t start up. One is named “test”. Since the starting elo of 1500 is near the top elo for bots, and the elo falls slowly since nobody plays many games against a bot that doesn’t work, in ranked play the bad bots tend to be matched against the stronger humans. That’s unfortunate.

human habits

In practice games, most humans seem to have favorite bot opponents, and don’t experiment widely. That’s interesting, and to me a little surprising. Most pick opponents that they usually lose to. I think either they are truly practicing, or else they’re taking it as a challenge. The strongest humans don’t have bots that they usually lose to, though.

I have the impression that there have been more ranked games over time, as a proportion of all games. Maybe players are getting familiar enough to feel comfortable with it. Many humans choose practice games every time, though, which is of course a perfectly good choice.

It’s curious, but human games are more stereotyped and less varied than bot games. At least at this level on SCHNAIL. (It’s not so at the highest level of human play.) Most humans on SCHNAIL seem to play the same base strategy every game, like the bot SAIDA does, with adaptations to what they’ve scouted.

Humans quit the game before any fighting in a surprising (and irritating) proportion of games. It’s impossible to know how much it’s due to “Oops, I messed up my split, try again,” versus “I’d better take this call,” versus “ack, why’d it freeze now?” From my point of view as a bot author, these are noise games that make the data harder to interpret.

Steamhammer’s play

When Steamhammer wins, it’s not due to better strategy (it’s worse than humans), or better tactics (it’s far, far worse), or better micro (better in some aspects, but overall worse). It’s occasionally due to a rush, zerglings or lurkers breaking in by surprise, but not often; humans are better at adapting and more resilient in defending than bots. Usually it’s due to better macro: “I lost this base, I lost that base, I didn’t kill any bases of yours, but I’m still ahead in workers. Say goodnight.”

Steamhammer’s inability to foresee an enemy army’s intent is a severe weakness. “Your army is over there, near my expansion? It’s out of position, I’ll move right up in front of your natural. Wait, you’re attacking my expansion now? Run run run to defend! Oops, too late.”

In games versus terrans, it’s clear that Steamhammer is now stronger with lurkers than with mutas. Lurker play is still crude, but the recent improvements have paid off.

gg when losing

As mentioned in the post the HumanOpponent flag, Steamhammer behaves a little differently when facing a human opponent. In particular, it gives up much earlier when losing. The rule is explained in the old post.

When I wrote that surrender rule, I doubted that it was tuned well. I have been pleasantly surprised, because it works great. In actual games, Steamhammer only surrenders when it is truly losing, and the timing is almost always reasonable: Late enough that it is also clear to the opponent that Steamhammer is losing, and early enough not to waste too much human time. I wish everything worked that well!

Next: I played around a bit with the SCHNAIL api. It’s easy to extract data in JSON format. I want to analyze it a bit.

BOSS build order visualizer

Here is another valuable new Dave Churchill tool. It doesn’t seem to come with a name, but it is a web build order visualizer which uses BOSS behind the scenes. (I’m told it is a new and more powerful version of BOSS.) You enter up to 3 build orders, BOSS simulates them to find their timings, and it diagrams each one. The picture shows how inserting a drone into a 9 pool gas build, to get back to 9 workers, can delay the initial zerglings.

the interface and build order diagrams

Partial instructions: + and - move a selected item up and down in the build order. X erases. X* erases the whole build. * selects the whole build. The various > and < buttons copy items between locations. In a diagram, hover over an item for info about its cost and timing.

It’s unfinished, and yet already valuable. Despite many limitations (and one bug I found, which has been fixed), I have already used it to analyze and optimize over a dozen builds. It’s much quicker than entering a build into Steamhammer and playing a game to get the timings. Some of the drawbacks in its current state:

  • No instructions. I’m told it will get instructions by the time it’s finished.
  • It does not support upgrades or research. That’s a big one.
  • It does not support stopping gas mining (like mining 100 gas for zergling speed).
  • It does not support pulling workers, even one worker to scout.
  • It does not support the zerg extractor trick.
  • If any of the 3 builds is invalid, you get a blank picture. There are no hints about what is wrong.

I found it best for low-economy builds. For zerg builds I tested to up 9 pool, its timings match well with Steamhammer’s execution. In overpool builds, only slightly slower, Steamhammer is able to insert an occasional extra drone or zergling without slowing down the timings. I expect that is because mineral locking makes mining more efficient than BOSS assumes. On the other hand, BOSS seems to believe that gas mining is a little faster than Steamhammer accomplishes it. That might be an issue of the time it takes to get workers into the refinery when it finishes. Even so, I can optimize a build (if it’s within the limitations) in the visualizer and then drop it into Steamhammer to see if further improvements are possible. It’s easier.

From a quick glance through the page source, it looks as though BOSS has been compiled into javascript using emscripten, so it executes in your browser. The visualizer works offline.

Good stuff already. How much better will it get?

STARTcraft

Dave Churchill is feeding his students Starcraft as educational food. And he is coming out with new software to make it even tastier.

True to his tradition of naming projects with puns on “Starcraft”, he created a minimal starter bot named STARTcraft. It’s in C++ for Windows; he promises a Linux version later. Its selling point is that it is extremely quick and easy to set up and start coding. Installing Visual Studio 2019 is the slowest part, and after that it should take only minutes to get the bot running: Download the repo, unpack Starcraft from a provided link, open a VS project to code, or run a batch file to launch Starcraft and the bot. A couple of long videos (2 hours each), linked right in front on github, go over Starcraft AI in general, and over STARTcraft and BWAPI programming in particular.

Lately, I have the impression that most new bot authors want to start from scratch and create a bot that is entirely their own. It looks to me like STARTcraft is ideal for that. When you run it, it sends its workers to mine minerals, builds supply when necessary—and that’s all. Well, it also does a little debug drawing on the screen, and some basic map analysis on startup, and keeps track of when each map tile was last seen. There are a small number of utility functions in MapTools and Tools. It is basically just enough to show that the bot is working and give small examples of how to do things. In fact, the few things it does, it does in a simple way rather than an excellent way. If you end up creating a strong bot, you will have rewritten the existing behaviors. It will be all yours.

I’m an old-timer, so I may not be the best at judging what’s good for a beginner. In the past I have recommended starting with an open source bot that is already capable and full of features, but today capable bots are complex and may be intimidating to start with. STARTcraft is simple. With the videos as documentation and the code as examples, and with a working starting point, it is almost as easy as it can be, and yet leaves nearly everything to you. Even if you have unique ideas about architecture, the bot is small and you can refactor it into the shape you like. Recommended.

Steamhammer’s special preparation for AIST S4

AIST S4 starts tomorrow. AIST is a different style of tournament from AIIDE, and I prepared differently for Steamhammer’s specific opponents. From the competitor’s point of view, AIST is a sequence of best-of-3 matches until you lose 2 matches—or win in the best-of-5 final. Steamhammer is unlikely to face the same opponent twice. With only a few games per opponent, Steamhammer’s deep library of builds is irrelevant.

I could have tuned the learning system to work well in short matches, as PurpleWave has done. Instead, I decided it was simpler to disable learning and use Steamhammer’s classic enemy-specific opening selection, which has gathered dust ever since learning was implemented in Steamhammer 1.4. Once I decided, I completed all the prep in a matter of hours. For each of the 5 opponents, I selected between 3 and 5 openings that were different and had high winning rates (except versus Stardust, which Steamhammer almost never defeats, where I picked what I hoped might have a chance). To choose, I looked at saved learning data for Steamhammer and Randomhammer zerg on BASIL, and for Steamhammer on SSCAIT, because each instance came up with different winners thanks to random sampling. Steamhammer will choose randomly from the openings for each opponent, according to probabilities I set, which sometimes vary depending on the map size.

For those who examine the tournament games, these lists may make the builds easier to interpret.

Stardust

  • 9PoolHatchSpeedAllIn (Styx build)
  • 10Hatch
  • 3HatchLingExpo
  • 3HatchHydra
  • 2x10HatchAllIn

BananaBrain

  • 2x10HatchSlow
  • 10Hatch
  • Over10HatchBust (zergling bust)
  • 3HatchMutaPure
  • OverhatchExpoLing

PurpleWave

  • ZvZ_Overgas9Pool (one hatch muta)
  • 11HatchTurtleHydra
  • 9PoolHatchSpeedAllIn (Styx build)

Dragon

  • AntiFact_2Hatch (2 hatch muta specialized to beat factory builds)
  • AntiFactory2 (3 hatch hydra into muta)
  • 2HatchLurker
  • 3HatchLurker

WillyT

  • 9PoolLurker
  • OverpoolLurker
  • 11Gas10PoolLurker
  • 11HatchTurtleLurker
  • ZvT_3HatchMutaExpo

Steamhammer has virtually no chance to beat Stardust. I’m guessing that Steamhammer has maybe a 20%-30% chance against PurpleWave or BananaBrain, and a better than even chance against each of the terrans. Notice the lurker builds, making use of Steamhammer’s stepped-up lurker skills.

Steamhammer’s combat sim smoothing

UAlbertaBot has a simple mechanism for hysteresis in combat: After retreating, it has a time limit before it will attack again. Combat sim results are unstable, and using the raw simulation results can cause vacillation from one frame to the next, which is Not Fun. When I changed Steamhammer’s tactics to cluster units and do combat simulation separately for each cluster, I had to give up UAlbertaBot’s mechanism. The clusters have to be formed anew every frame, so they cannot carry over data from frame to frame. Ever since then, Steamhammer has had jittery tactics, often engaging and disengaging its units at computer speed. It hurt in all matchups, and to me it was most obvious in zergling on zergling micro in ZvZ games. Combat sim smoothing is my first attempt to pull myself back onto solid ground.

It seems to me that Steamhammer’s smoothing is a complicated idea, but the implementation is short and hides its internal details well. It’s a similar idea to one in PurpleWave: Instead of attaching data from previous frames to the cluster, which won’t exist in the next frame, attach it to the individual units. Then the next cluster, freshly created in the next frame, can extract the data from its own units, which are often different from the units in any older cluster.

The only alternative I see is to attach data to locations on the map instead. Both schemes rely on locality, meaning that units move around relatively slowly and affect only their immediate surroundings. Both schemes risk breaking down if units are suddenly transported, recalled by an arbiter or sent through a nydus canal. In those cases you should probably erase their saved data.

Here’s how I chose to do it. A cluster can do at most one combat sim per frame, and no unit participates in more than one cluster. For each cluster that does a combat sim, it sends the cluster’s set of units and the combat sim results (“we’ll win” or “we’ll lose”, nothing more) to Micro::setAttack(). The Micro module already keeps info about each unit’s orders and other info in a map of MicroState objects, so I thought that was the logical place. For smoothing, MicroState stores one floating point number for each unit, which is a smoothed average over time of the unit’s “we’ll win” (represented as 1.0) and “we’ll lose” (represented as 0.0) values. The algorithm is exponential smoothing, which is the next closest thing to trivial.

The cluster, having informed Micro of its latest combat sim result, then calls Micro::getSmoothedAttack() to get the smoothed combat sim result, which is what it actually relies on. Micro simply averages together the smoothed values for the cluster’s units and compares to 0.5. Done.

It’s smoothing only, there is no hysteresis. If the fight is a close call and remains so over time, Steamhammer may still oscillate between engaging and disengaging. But I think it is much rarer than before. I see the smoothing as an important improvement.

Many variations are possible. This is my first cut, and I did no testing of alternatives. Maybe it is better to smooth the actual combat sim scores, rather than only the binary win/lose results. There are ways to add hysteresis to the scheme. I’m sure improvements are waiting to be found. I may look for them.

Next: Steamhammer's special preparation for AIST S4.

Steamhammer’s pathfinding

UAlbertaBot comes with distance maps: Give a destination tile position, get back a grid with the approximate ground distance from each tile on the map to the destination, or -1 if you can’t get there from here. Since forking Steamhammer from UAlbertaBot, I’ve added to and refined the subsystem, and used it for more purposes. For example, Steamhammer uses the ground distance to decide when to transfer workers to a new base so that they arrive just after it finishes. Pathfinding is an obvious use of distance maps, so maybe it’s surprising that I didn’t implement it until now.

the implementation

The low-level implementation is simple. The move command the.micro.Move() accepts an optional distance map. If provided, it calls on a simple algorithm that searches the distance map and finds a shortest-distance path segment 8 to 12 tiles ahead of the moving unit. MoveNear() and MoveSafely() also accept the optional distance map and pass it on to Move(), which does the work.

Plumbing the distance map through the abstraction hierarchy is the hard part. I chose to allow the squad’s order (class SquadOrder) to optionally keep a distance map. Basically, at any given time a squad may either have a distance map to its target, or not. If it has a map, then it is passed along on every movement order for the squad’s units. CombatCommander decides on the squad orders and generally gives a distance map to ground squads, and none to air squads which can fly straight to their destinations. Most squad targets are bases, and it happens that Steamhammer precalculates a distance map for each base. So a squad order to target a base uses the base’s distance map for free, while a squad order to target an arbitrary location calculates a new distance map which belongs directly to the squad order. I’ve been planning that detail of the implementation for years.

Pathfinding is used for squads with ground units, for the scouting worker, and for sending a worker to construct a new base. It is not used for constructing other buildings (which are usually nearby though not always) or for transferring workers between bases.

If the debug flag to draw squad information is turned on, a squad which has a distance map is listed with a “#” symbol before its name. The “#” represents the grid of distances.

results

Pathfinding works well in test games, though not perfectly. The paths are not true shortest paths (see “disadvantages” below), but close enough for now. In test games on Medusa, units of the Ground, Recon, and Watch squads navigate well, no longer getting stuck in the dead end back doors of the bases. On the other hand, I commonly saw a small number of transferred drones stuck there. One oversight is that some units still tend to get stuck behind the neutral buildings on Benzene. Benzene is not in the AIST S4 map pool, so I didn’t notice.

I also need to update the precalculated maps when map obstacles are destroyed. I’m sure I’ll iron out the details in time. It didn’t seem vital for now.

advantages

Once a distance map is calculated, it’s cheap to follow not only a shortest path for one unit, but any shortest path for all units. If you like one shortest path better than another for any reason, you can choose it on the fly.

A distance map need not be calculated from a single destination tile. It can start from any set of destination tiles, so that you can do things like find the shortest path to enter a region, or choose the closest of 2 equal goals. A distance map need not record literal distance; it can pretend that a dangerous tile is larger than others so that you tend to path around it when you can. Distance maps of different kinds have many potential uses.

  • Ground unit pathing through terrain, the implemented feature.
  • Ground or air unit safe paths, to avoid all known risk for scouting, or to discover containments.
  • Ground or air unit least-danger paths, to reach a defended target with minimal damage.
  • Ground or air paths which are out of sight range of known enemies, for sneaking around.
  • Paths to safe firing zones, so we can attack a defended target from outside its defences.
  • Maintaining a contant distance from an objective, such as the convex hull of a group of enemy units, for arcs and surrounds.

disadvantages

As implemented, the distances mapped are not true unit movement distances, but Manhattan distances. Units moving diagonally, say from bottom right to top left, tend to move not diagonally, but first to the left and then upward. I intend to correct it, but haven’t decided on a plan yet.

Distance maps are a bit bulky and slow, and usually involve calculating a lot of paths that you won’t go anywhere near, so you don’t want to calculate too many maps each frame. It’s better to reuse them as much as possible, which adds complexity. UAlbertaBot comes with a distance map cache, but its replacement policy is “periodically throw out all entries in case some may be unused.” Something a little more sophisticated is called for, especially if you want to store different kinds of distance maps in the same cache.

If you want to map something that changes frequently, like detailed enemy firing zones, you can either recalculate the map frequently—which is fine up to a point—or accept that it will often be a little outdated—also usually fine.

The disadvantages don’t seem severe to me. Distance maps gain their flexibility by doing more calculation than is necessary for any one use case—guess what, work harder, earn more. It does call for a little attention to efficiency.

Next: Combat sim smoothing.

new bot Vyrebot

The latest Steamhammer is uploaded to SSCAIT, descriptions of its new pathfinding and new combat sim smoothing are in the wings, Dave Churchill has been quietly turning out new tools, but it all must wait because we have a new bot! And it’s an unusual one.

Zerg Vyrebot accurately describes itself as “?????????” It was disabled on SSCAIT after 3 games, but remains active on BASIL for now. It follows a unique game plan. Early on, it makes a single sunken colony for defense, placed without regard to covering all buildings or to the enemy’s line of approach. It slowly techs up to lurkers for further defense and expands to its natural. Though slowed by gas shortages as it adds ever more lurkers, plus a spire for scourge in case it needs air defense, it progresses to hive, makes defilers, and researches consume. Then the game is on: It attempts a coordinated lurker-swarm attack! I thought it was an advanced skill, but no, for Vyrebot it’s the first skill that it attempts to master. And, though unpolished and slow-moving, it’s not bad at it, far better than I expect for a brand-new bot with no other significant skills. Dark swarm is difficult to use well, and I’ve seen tscmoo play more weakly with it.

Vyrebot has a public github repo. A comment mentions YouTube videos, but I didn’t find any.

The Vyrebot game that I liked best is Vyrebot-Slater on Heartbreak Ridge, played on BASIL. But of course other games are less impressive. Usually Vyrebot loses, as you should expect for a bot whose best skill doesn’t come into play until the late game. Any opponent that attacks early, or that attacks in the midgame and knows how to defeat lurkers, or that attacks by air before the late spire finishes, is likely to have an easy time of it. If you want to win, then early game skills are more important than late game skills, because you always have to play the early game and only sometimes reach the late game in a survivable position.

Still, I appreciate a creative approach. Go Vyrebot!

Steamhammer 3.4.8 for AIST S4

I have submitted Steamhammer 3.4.8 to AIST S4. Here is the change list, nice and long considering how long there was to work on it. The deadline isn’t for hours yet, so revealing my Dark Secrets early might theoretically come back to bite me, but... eh, it won’t bite hard. I’m still concealing my specific preparation for these opponents.

I tried for important advances that wouldn’t take long. I think I met my promise that Steamhammer will play a more “lucid” game, but let’s see how many fresh bugs I didn’t notice! The biggest improvements are pathfinding for ground units, smoothing of combat sim results, and more supple lurker behavior. My intention has always, ever since I started Steamhammer in December 2016, been to work on mutalisk micro before lurker micro, because it is more important. But I let it pass for too long, and now other bots have strong muta micro. I’ll get back to mutas (they are still more important), but I always want to do something different from everybody else, and now I want to work on lurkers first.

The irradiated squad is a nice improvement, too. Anyway, read on.

map analysis

• Reject starting bases (as identified by BWAPI::Broodwar->getStartLocations()) which have no minerals; they are no longer considered bases, much less possible starting bases. I think these are always observer slots. The AIST S4 version of Aztec has observer slots. Formerly, Steamhammer accepted whatever BWAPI told it and created a Base object for each one, marked as a starting base. The error had surprisingly little effect on play (Steamhammer is not tempted to expand to valueless “bases”), but it did cause sneaky mistakes. One is that a starting base is assigned a natural if conditions are met, with the result that the center bases on Aztec were considered naturals of some of the observer slots, and that information could be used for certain decisions, like where to make a hidden base. I’m glad I found this by looking at the maps, because the mistaken decisions might have been a nightmare to diagnose!

scouting

• The scout worker is released whenever friendly combat units are near. This is the final relaxation of the originally tight conditions for releasing the scout worker early, which I’ve been progressively relaxing.

• If the enemy has no known anti-air units, send an overlord to each base on the map to keep watch. Formerly, the rule was “if the enemy is not known to have tech to make anti-air units”, which was rarely satisfied outside the early game when there weren’t overlords available to distribute.

squads

An irradiated squad centralizes the code for managing irradiated units and implements fancier behavior. Every irradiated organic unit (the ones harmed by the irradiate spell) goes into the squad, except for queens which have enough energy to cast and defilers after consume is researched—they will try to cast something before dying. Formerly, an irradiated unit that could burrow would, and that was the extent of the reaction (the successful reaction, anyway; mutas had code to separate out an irradiated muta, but it mysteriously broke). Now, an irradiated unit that is near friendly units will burrow if it can, or try to flee from its friends if it cannot. If it is near enemy units, it will approach to expose them to the radiation, and also attack if it can. If neither, it will run and do what scouting it can. Some of the behavior looks questionable, and I suspect bugs, but it’s good enough for now. The reaction of unirradiated units to nearby irradiated units is unchanged—only workers try to protect themselves, others carry on as usual.

• A newly-created squad did not pass along its order to its own micromanagers until it was updated. It was an oversight inherited from UAlbertaBot. The main effect is that a defense squad would not begin to act until exactly 8 frames after it was created, so defense was always a little bit late.

• The debug display for squads now shows two new pieces of information: A # symbol if the squad is using pathfinding, and the squad’s status string. The status string is sometimes unchanging, sometimes informative. For example, the Scourge squad has status Attack or Stand By, depending on whether enemy flyers are targetable.

• There was a minor bug in deciding which enemy unit last-seen location to visit next after all known enemy buildings are destroyed. The fix has little effect.

other ops and tactics

Smooth attack/retreat combat sim decisions over time. I think this is the single most important change. Micro is less jittery and more decisive. I’m leaving out details for today; it’s worth a separate post.

• I found and fixed several more bugs related to spell units. The most important misbehavior was that Steamhammer treated an enemy comsat scan of a base as an attack that needed to be fended off. Every time terran scanned a base, a couple of mutalisks might peel off the flock and head there to defeat the scan. They never failed!

• I added time hysteresis to the defense squads, on top of their existing range hysteresis. After the enemies have been seen off, the squad waits out a time limit before it is disbanded. I was not convinced that the feature helps play, so I set the time limit to only 1 second.

• When a cluster of units is ordered to retreat, it may decide—depending on a simplified geometry calculation involving a risk radius—to “retreat forward” to join with friends. This helps a small cluster join up with a big cluster that is already in a fight. I decreased the risk radius, except in the case where the enemy is terran and has sieged tanks. It should help small clusters retreat forward more often.

• Earlier, I introduced a bug into FAP by adding an incorrect MAX_DISTANCE constant. I had forgotten that the distances are squared. Fixed.

micro

Pathfinding for ground units for calls to the.micro.Move(), the.micro.MoveNear(), and the.micro.MoveSafely(), (but not the.micro.AttackMove()) at the option of the caller. I’ll post details tomorrow or so. It was easy to guess I would do this now, because Medusa is in the AIST S4 map pool, where Starcraft’s built-in pathfinder likes to pile up units at the blocked back doors of the bases.

• A unit trying to move safely (with the.micro.MoveSafely()) does not try to avoid interceptors, only carriers. Trying to avoid swooping interceptors causes erratic movements, not escape movements.

zerg

Lurker behavior is smarter. When I first implemented lurkers in Steamhammer 1.3 in 2017, I found that if they obeyed the combat sim like other squad units, they were nearly useless; they did not understand when to burrow, or where to burrow, or when to unburrow. The SparCraft combat simulator I had at the time did not support lurkers. Steamhammer could not tell when the enemy had detected them. So I gave lurkers spartan hyper-aggressive tactics: Always attack, burrow at max range versus a target that can shoot back and directly next to a target that cannot, and unburrow when no target is in range. I’ve made only minor refinements since, because it worked surprisingly well, especially against terran bots. But the crudeness shows, and hyper-aggressive is often hyper-stupid. A lone lurker would boldly attack a line of cannons. Steamhammer has lost a lot of lurkers for free.

Today Steamhammer is a capable squad commander, and it can judge pretty faithfully when a lurker remains undetected by the enemy and should remain safely burrowed despite (meaning because of) the large enemy force on hand. Lurkers in a defense squad remain hyper-aggressive, so that they do not hesitate to eliminate enemies from the zerg base. Other lurkers now advance or retreat together with the units in their cluster, with some exceptions for retreat (see below) so that lurkers don’t unburrow too often. Lurker play remains clumsy, but it is far more flexible than before, and better overall.

• A lurker that Steamhammer believes is undetected will not retreat as long as it is in range of a target. It will remain burrowed, or it will burrow then and there instead of retreating. I made this change in an earlier version, but it had no effect until now because lurkers did not retreat.

• Failing the above check, if a burrowed lurker is asked to retreat then there is one more check: Will it survive long enough to unburrow? It calculates an expected survival time in frames and compares it to the time to unburrow plus a safety margin. If it won’t live that long, it doesn’t bother unburrowing; maybe it can get a last shot in. I do the same calculation for sieged tanks, with the unsiege time, so that a tank beset by zealots still has a chance for a last shot at some distant dragoon.

• I fixed a bug in the hidden enemies check; it handles an invalid missing ui.unit correctly. I don’t think it makes a practical difference with the existing codebase. The hidden enemies check prevents unburrowing when the lurker is undetected and, if it unburrows, at risk of dying to a cloaked unit, or a known unit out of sight on high ground. This check often (not always) prevents the cycle: Burrow on low ground just in range of a bunker up the ramp -> bunker stops shooting because the lurker is no longer visible -> the bunker is no longer visible to the lurker -> the lurker unburrows -> the bunker starts shooting again and becomes visible -> the lurker burrows....

• When possible, a lurker, guardian, or devourer will morph out of enemy view. When within enemy static defense range, it will not morph at all—at least theoretically; I’ve seen it happen so I think the check is not accurate. Lurkers should more often surprise the enemy, and cocoons should be shot down less often.

An urgent sunken to stop vultures, or an urgent spore to stop wraiths or corsairs, is made quickly, rather than, oh, whenever the bot happens to get around to it, might be any minute now if we’re still alive then. The slowness was due to a bad design decision I made in reworking the code in the SSCAIT tournament version.

Insert a fresh overlord after a spore colony, not before, when making supply and a spore is next in the queue, and similarly for the evolution chamber prerequisite. Formerly, when corsairs ravaged the overlords, Steamhammer gave priority to getting its supply back into the green. The corsairs had only to camp the zerg base for protoss to win.

• When scourge finds that its target is very close, it attacks even if the combat sim says to retreat. Scourge uses a combat simulation, excluding air units, to avoid ground fire. Often it would be on the verge of shooting down a corsair, but a dragoon was a little too close.... Steamhammer has missed a lot of kills that way.

• The defensive sunken versus cannons is tuned slightly.

openings

• There are no new or changed openings, only a change to how one opening is configured to be used. I saw the OverpoolTurtle build, a bot exploit opening that is objectively horrible, played in one game against a human on SCHNAIL, and that was one time too many. I removed it from the matchup and counter configurations, so it should be played very rarely unless it is found to succeed, and almost never against a human.

configuration

• New flag for Crazyhammer. Setting Config::Strategy::Crazyhammer to true causes Steamhammer to choose its openings purely at random from its large library, ignoring anything it may have learned. It’s used for Crazyhammer on SCHNAIL.

• New debug flag Debug::DrawLurkerTactics draws the name of the current default lurker behavior, either “Aggressive” or “With squad”. It doesn’t convey any information at the moment, since whenever there are lurkers they are “With squad”. I plan for lurker behavior to become more varied and complex, and then the debug flag will be useful.

unused stuff

• The movement sim is included and potentially valuable, but unused. There is code in FAP and in CombatSimulation.

• Fixed a primordial crashing bug in enemy unit clustering, making it also potentially valuable. But it’s turned off for now.

Next: Pathfinding.

movement simulation

Steamhammer is nearly prepared for AIST S4. I made 3 major improvements, plus smaller ones. Not everything I tried succeeded, though. Today I want to write about a new feature I implemented that is definitely valuable, and that Steamhammer will find uses for in the future even though my first use case didn’t work out as I hoped.

Steamhammer inherited from UAlbertaBot a weakness in retreating. It is Steamhammer’s longest-standing serious weakness. The point of retreating is to get away from a fight, so the units get move commands and ignore enemies they meet on the path to their retreat point. Nearly all cases where Steamhammer’s units run into or through or past enemies without fighting are because it is trying to retreat them. There are spectacular failure cases, like running ultralisks through a field of cannons and losing them for nothing. My past attempts to fix it have been half-hearted, since I want to delay coding a full tactical analysis until later. I thought I’d try something more courageous.

So I implemented retreat simulation in FAP. Moving is a lot easier than fighting, and it didn’t take much code. Pass in a target position, and our simulated units move to the position while the enemy’s keep chasing and shooting as usual, with no change to their side of the simulation. We’ll say the retreat “succeeded” if the fleeing units’ losses were lower than some percentage.

My idea was: If the regular combat sim said to retreat, then I ran a retreat sim in the same frame. If the retreat failed, then don’t run away, keep fighting regardless. The retreat sim runs fast because it is simpler, and also I cut its duration to only 2 seconds. I didn’t meet any problems with the frame time limit. In principle, the retreat sim can conclude “the cannon will kill us even if we run away, so keep hitting it,” and notice when the retreat path is blocked, and distinguish a faster force that can escape from a slower force that the enemy will chase down (yes the speedlings will kill your marines for free if they run, better to make a last stand).

In practice, the retreat sim might be worth it on average, but I was not convinced. The sim results were as noisy as ever, and there were times when it hurt. Also, in some situations it’s conceptually the wrong thing to do. Having the “retreating” ultralisks fight cannons is better than running past the cannons, but better yet is to understand the situation as an attack rather than a retreat, and to stand aside until you can win. And that seems to call for the tactical analysis that I was saving for later.

I called the internal routine in FAP not retreatsim() but rather movesim(), because it can answer many questions about when movement is safe.

  • Will this runby succeed in running by?
  • How far can the transport go before it must unload?
  • How many scourge will survive to hit their target?
  • Will this plague/broodling/etc. succeed, or will the caster be killed first?
  • If I mind control that unit, will it live to join my army?

And with a little more elaboration, questions like

  • If it lives to cast, will the defiler/queen/etc. also escape safely?
  • Is there time to unburrow/unsiege, or should the immobile units remain as they are?
  • Can the mutalisks pick off another marine and still get away without losses?

I think it’s a safe bet that Steamhammer will be answering some of these questions before AIIDE this year.

new bot Bobot

We got another new bot! Newcomers have been thin on the ground of late, we must treasure them. Terran Bobot’s description says “little bot I made to have fun and also to go into machine learning in the future. By william sokol”.

“Bobot” seems to be a popular name that has been used by unrelated projects. I found a repo of a BWAPI bot named BOBot by Antohi Mihai Razvan, which appears to be a different bot, and much older (it uses the old problematic BWTA map analysis library, while our Bobot uses BWEM).

Bobot has lost nearly all games so far, which is not uncommon for a brand-new bot made from scratch by an inexperienced author. It did defeat Marine Hell a couple times on BASIL, winning on points when the games timed out. Every bot author who sticks with it soon starts to win games. Bobot seems to have a basic idea of how to build a base and produce units: It makes barracks, academy, natural base, and 2 factories, then produces marines and tanks. I haven’t seen it get any medics or academy upgrades; the academy is apparently for scan and nothing else. It does not research siege mode for the tanks. Bobot doesn’t scout for the enemy, and apparently doesn’t actively attempt to attack enemy bases. Its combat skills appear to be limited to rallying units in front of its base and then, when enemies appear, attack-moving in their direction. In the best case, its units may be lured to an enemy base and destroy it.

Overall, Bobot looks like it is very early in development. This may be a test upload to make sure it runs, before the author gets down to serious work.

One of Bobot’s better games is Bobot vs GarmBot by Aurelien Lermant on Destination. Bobot lost by crashing, but its units were lured into destroying a number of zerg bases first.

Be sure to have fun, William Sokol! I look forward to seeing what machine learning ideas you try.

another Steamhammer-Krasi0P proxy game

Back in last July and August, I wrote up a couple of Steamhammer-Krasi0P proxy games, proxy vs proxy just to show off the new skill, and proxy vs turtle, which was a festival of absurdity. A few days ago, Steamhammer played another proxy hatchery against Krasi0P, this time one that looked almost tailor-made for the opponent. In reality the build was tailor-made to defeat TyrProtoss’s cannon-and-build-up-at-home strategy, although Steamhammer has never tried it against TyrProtoss. It was sheer coincidence that it was also suitable to counter Krasi0P’s cannon-contain strategy (not its cannon-at-home strategy this game). Though I should add, I keep specialized builds around in part to discover coincidences like this.

both proxies are up

The game is already 6 minutes old. Krasi0P, seeing the proxy, built only one containing cannon, which you may be able to make out as a purple dot on the minimap. That may be why protoss has extra minerals. Steamhammer, when it saw the cannon in turn, decided not to take its natural but to send a drone to the upper left, where it took a “hidden” base. Steamhammer does not want to be stuck inside a containment, it wants to be on both sides, and here it forgot that it already was on both sides. Protoss chose to tech up and defend itself with more cannons before moving out with its first dragoon, and zerg is just starting its first sunken with good timing. The timing is an accident, since the build was made for a different opponent playing differently!

hitting that pesky cannon

Steamhammer made lurkers and, before anything else, sent them to clean up the isolated containing cannon. It would have been more efficient to send the hydras before morphing them into lurkers....

moving in for the kill

Steamhammer added more sunkens and a spore to prepare for TyrProtoss’s move out with a large army including an observer. Krasi0P did not have a large army or an observer, and soon fell to three hatcheries worth of zerg onslaught (on the minimap you can see units approaching from both other bases). Krasi0P seemed to try to adapt by cutting its containing cannons down to one, but did not understand the situation and reacted slowly. Steamhammer didn’t understand either, and only chose an appropriate plan by chance, but did at least execute its build efficiently.

They aren’t visible, but in each picture, outside the frame, there are drones mining at the proxy. Even after making 4 sunkens and a spore, 3 drones remained to mine there. Another funny game between these two.

the kitchen sink

A SCHNAIL game. This human player was able to cope with every aggressive trick Steamhammer tried, and it tried several, but not able to keep up macro at the same time.

finishing moves

Zerglings and ultralisks are under the dark swarms, invulnerably shredding everything. Of the ensnared marines to the left of the swarm, the majority are also plagued. Only the most recent spell shows.

When its economy is strong enough, Steamhammer switches quickly from midgame play whose goal is to hold on and not fall behind, into the endgame with power moves. As a human player, how does it feel to face so-so play for most of the game, holding after no worse than a bit of a scramble, and then be abruptly overwhelmed by a prodigious army that seems to have all the tech and be able to do everything simultaneously?

In the following game between the same players, the human was the aggressive one, moving out and repeatedly destroying zerg expansions. Steamhammer never built up its endgame army. But with the terran army away from home, mass zerglings overran the terran bases. Keeping up with AI macro is mechanically demanding for the human player. I expect it’s good practice, though.

AIST S4 prospects

Here are the participants in AIST S4 sorted by BASIL elo, with the latest BASIL update as a guide to gauge how much the bot may yet be updated by the submission deadline of 15 March. If it has been a long time since the last update, the author may have taken the opportunity to make big changes, so the elo is a less reliable guide.

boteloupdate
Stardust307229 Jan
BananaBrain29401 Mar
PurpleWave287714 Jan
Steamhammer27281 Feb
Dragon26871 Oct
WillyT256526 Feb

Elo is pretty good, but even the best forecast can’t predict the results of a knockout tournament with random seeding. If we take elo as perfect, then any bot in the bottom half might get a tough pairing in the first round and fall to the loser’s bracket, then face another tough pairing. In reality elo is not perfect and even a top player might be knocked out early with bad luck in the BO3 matches. Also, 6 players do not make an exact power of 2 bracket, so I expect that some bots will get a bye the first round and the remainder will have to play an extra match, facing a longer road. The bracket works out if 2 bots get a bye the first round, so that 4 play in the first round and 2 drop to the loser’s bracket. Then round 1 and round 2 of the winner’s bracket both have 4 players.

Nevertheless, Stardust is clearly the favorite to win. We probably have a good handle on BananaBrain’s strength, since it was updated only a few days ago. PurpleWave I hope will have fixed the issues that harmed its performance in SSCAIT—unfinished improvements, we’re told—so it may be able to pass BananaBrain and meet Stardust in the final. Dragon may or may not get a big update, since it has not been reuploaded since October. If it does, it might suddenly become a contender.

Steamhammer, I don’t mind saying, has important updates and should play a more lucid game than the current BASIL version. On the other hand, I have stayed true to my intention of treating the tournament season as over—although it’s not—meaning that I disfavor safe improvements and favor work on basic infrastructure and new features that may need tuning. I don’t have time or resources to test intensively, so I accept risks of bugs and unexpected consequences and poor tuning. Either way, success or failure, Steamhammer may cross the plans of any bots that try to prepare specifically against it. I think my odds are good of, at last, avoiding the bottom spot in AIST. And I hope to beat one of the protosses.

low level bugs

In its first draft, a new feature in Steamhammer caused a crashing bug. After several runs to collect information, it looked like a bad pointer or a memory corruption bug. Thank you C++, we enjoy unsafe languages above all others. With more poking, I identified a memory safety issue and realized that it would take some rejiggering to fix. It took me several hours to do minor rewrites up and down long call chains and verify that I hadn’t missed any links. Finally the code was tight, everything was double-checked, it compiled correctly, and I ran the first test—and it failed with the exact same symptoms. It happens to everyone, right?

Well, the show must go on. I configured tests to all run on one map for efficient debugging, and brought out the loupe for a close look. It took me three days, I wrote self-test code to catch errors as early as possible and extract their secrets, I had to learn (what seems to me) Lovecraftian C++ arcana that I would prefer not to know the existence of much less to understand, but I solved it, and kept the code nice and efficient too, and my new feature came to work perfectly on the test map in a wide variety of situations. That took a lot longer than writing the first draft. OK, now to test more widely. On the very next map—the self-test immediately caught errors. It happens to everyone, right? Right? Right? Cue “They’re Coming to Take Me Away, Ha Ha!”

This bug turned out to be trivial, though.

I once worked with an intern who wrote the worst code I have ever seen. Not only was it tangled and barely decipherable, it was ingeniously self-modifying, and not with address variables but with hardcoded address constants so that it depended on running at a fixed address... even though the software was planned to be burned into ROM. Unbelievable but true. My first step in fixing it was to throw it out. I guess I should be proud of myself as a stable and careful programmer, since I still have some hair and I rarely lose days of time to low-level bugs.

But I have to say, if I wanted to have fun in an unsafe low-level language, I would use a fun unsafe low-level language, like FORTH.