Creating a Starcraft AI – Part 29: Biopuppers in action

(This is an ongoing series. This part is enjoyable without previous knowledge, but if you’re new, I suggest you start from the beginning)

First part of this series | Previous | Next | Index

After so much delay, the time has come to use my pathfinding. It is not perfect, it is not performant as it could be, but it’s usable. My original idea was a 4-Pool Zergling bot, that has threat-aware pathfinding, and can rush very effectively. This is obviously a strategy that can be countered, but I believe I can iterate on it further.

Before I go on implementing that, let’s have a quick overview on how I handle unit management. Every unit has a UnitManager class assigned to it, and specific unit micro/macros are handled in subclasses of that. So far I have a UnitManager and a ZerglingManager. A lot of functionality can be applied to almost any unit, so this structure makes sense. The UnitManager is not abstract – some units will probably not have their own subclass at all. (Keep in mind that buildings are counted as units in this context).

As expected, this is just a bunch of if-else conditions at the moment. Well, not even a big bunch, to be honest.

Képtalálat a következőre: „dumb robot”
Fun fact. AI stands for “Accumulated If statements”

Learning from past experiences, I will consider performance here. I most certainly do not need to care about every unit in every frame – so some kind of “skip frame” option needs to be added.

This is the complete zergling behaviour of JumpyDoggoBot at the moment. Pay attention, it’s very hard to understand.

    @Override
    public void execute() {

        if (this.role == Role.SCOUT) {
            if (unit.isIdle()) {
                scout(true);
            }
        } else if (this.role == Role.FIGHT) {
            if (unit.isIdle()) {
                unit.attack(targetPosition);
            }
            //KEKEkE
        }
    }

I can’t really improve perfection, but I shall try. The purpose of threat-aware pathfinding is to get to my target, preferable without damage. But first, I need to have actual targets. And if there are multiple ones, decide which one I want to attack. The behavior I’m trying to achieve is “get the workers asap”.

I already keep track of enemy units, so just getting the closest worker is not that hard. But in the meantime, an enemy unit may appear, and I want to avoid that. And if there are no enemy units present, I may want to scout to find some. There are some conditions where I want to deal with enemy combat units before workers. Vague concept, I know.

First, I’d like to keep track of the possible targets. These are not “all enemy units”. That would be redundant, and an unnecessary overhead. I want a list of targets, and an importance to them – This is something a priority queue was created for.

I do need a Target object that keeps track of the relevant data. For now a unit ID, and an importance number is enough. With BWAPI5, one promised change is for the enemy unit IDs – Currently, if I see a unit ID disappear, then reappear, it is the same unit. With BWAPI5, it will not be so, the IDs will be assigned on discovery. This will be a concern later, but most of the logic can be implemented without considering it.

I actually want the higher values to be at the head of the PriorityQueue this time.

public class Target {

    private int unitId;
    private int importance;
//
    @Override
    public int compare(Target o1, Target o2) {
        if (o1.getImportance() > o2.getImportance()) {
            return -1;
        } else if (o1.getImportance() < o2.getImportance()) {
            return 1;
        }
        return 0;
    }
//and in the ZerglingManager
    public ZerglingManager(Unit unit) {
        super(unit);
        targets = new PriorityQueue<>(new TargetComparator());
    }

Let’s take a step back. The threat-aware pathfinding can be used for scouting as well. One problem is, what happens if there is no path? Should I go to the closest point? Or try to do something else? If I go the closest I can, then I should exclude the original target tile, and the approach point as well from the scouting. Oh, and since I have multiple units (preferably a metric fuckton), I shouldn’t call the pathfinding too often.

Not a lot of stuff to balance at all! I will start by improving the scouting. A quick reminder: Scouting uses TilePosition level resolution, so it’s faster in the first place. Every tile has a value, and when scouting, units take the highest, and try to get there, currently with only the built-in move command. Now, let’s examine what happens, if I switch that to JPS, and get there quicker.

There are multiple constraints while doing that. The obvious one is that – as demonstrated before – JPS costs a lot, so we can’t call it that much. An easy solution is just to have a global counter every frame, and not call JPS if we reach that, and reset the counter every frame. Okay, good enough, but what are our biopuppers to do when they can’t call it? The obvious answer is nothing. One frame isn’t that much, and this is just a part of one of the behaviors a unit can exhibit.

Another one is just generally being a dumbfuck

A path is also something that is reusable. If there are a few zerglings close together, they can take the same path, no problem. Now, defining “close together” is yet another can of worms, so let’s just postpone that. (This article is basically about procrastination)

Turns out, I already implemented a kind of threat avoidance:

        while (scoutingTarget == null) {
            for (TileInfo ti : Main.scoutHeatMap) {
                boolean avoidTile = false;
                if (avoidthreats) {
                    //THis just ensures that the unit don't pick it as target.
                    WalkPosition scTile = ti.getTile().toWalkPosition();
                    if (Main.threatMemoryMapArray[scTile.getX()][scTile.getY()] != null || Main.activeThreatMapArray[scTile.getX()][scTile.getY()] != null) {
                        avoidTile = true;
                    }
                }

                if (!Main.scoutTargets.contains(ti.getTile())
                        && (unit.getType().isFlyer() || ti.isWalkable()) //Air units don't care about walkability
                        && !Main.bwem.getMap().getPath(unit.getPosition(), ti.getTile().toPosition()).isEmpty()
                        && !avoidTile
                        ) {
                    scoutingTarget = ti.getTile();
                    Main.scoutTargets.add(ti.getTile());
                    break;
                }
            }
        }

Which is not the same as the threat-aware JPS – this just excludes threatened tiles. I tested it out a few times, and it’s… actually quite sufficient. It can be improved upon, but this is quite fast, and works.

I can move on to the target acquisition part. One of the greatest questions of Starcraft bots is when to attack – the answer is always kekeke no one really knows. If there are targets in the unit’s list of, ehm, targets, we’ll try to pick one. This should by done by the ZerglingManager.

Zergling manager assigning targets, cca 2019 (colorized)

The prime objective is to get the workers. Since those have threatened areas as well (they can attack, after all), we need to exclude those at some point. The target assigning problem is further compounded by squad mechanics. Usually there is more than one zergling present, and I want to focus on some unit or another.

Targets can also be hidden by fog of war. That does not mean that I want to give up the chase. But since I can’t access the unit when it’s not visible, I’ll keep track of it by a very simple approach.

   //In the Target class
    private int lastSeenX;
    private int lastSeenY;
    private UnitType type;

Also, I can make use of the threat memory here for additional info, since that stores info by unit ID, among other things.

I’m inclined to implement the squad logic as well, or at least start to do it, but I already have too much todos. For now, I’ll experiment with the whole army as one group, and will build on that. Acquiring targets is one thing, but when to clear them out? When the targets die, that’s an obvious case, but enemy units can die off-screen, so that’s not 100% reliable. Also, if it appears in the other side of the map, that might not be the best. I believe this is the use case for the importance statistic.

So when I see a unit, there are the following factors to consider when assigning target importance:

  • Attackability. If we can’t attack it, then don’t even bother. This can mean the attacking unit can’t attack ground/air.
  • The target is untargetable (bruh), like invisible or stasised units, or stuff that are technically units, but should not be considered. (Like scanner sweeps, or reaver scarabs)
  • Target distance. Usually the closer the better, but we have to consider actual walking distance, with obstacles.
  • Target type. We want workers, and we want to avoid attacking spider mines with zerglings.
  • Target health. Health has no bearing on a unit’s effectivenes, a full hp unit deals as much damage as a 1 HP one. Health is a blanket term here, what I mean is a combination of “ablative” properties, like health, shields, and defensive matrix.
  • Target armor/defenses. Heavily armored targets take more hits. That, and the HP should form some kind of statistics, which basically represents “how many attacks do I need to kill this unit”)
  • Terrain. This is probably the hardest. Just a few considerations, but I’m pretty sure there are more: Ramps, high ground, terrain doodads (Did you know that vs units under trees you have a miss chance?), and even choke points.
  • Strategic importance. Doesn’t matter if I can do a lot of damage, if my base is getting destroyed in the meantime. This is a feature for further down the road, but still, a serious consideration. In any case, it’s not the role of the UnitManager to deal with this.

Did I just get into another rabbit hole? Most definitely I did. Rabbits are my people now. I’ll explore, and implement these concepts in the following installments of this series.

Thanks for reading! If you liked this article, you can subscribe to my mailing list (in the box on the right), follow me on Twitter, or Facebook, or check the RSS feed on the top right.

Also, I’d like to share an affiliate link with you – I’m not directly sponsored by Hired.com, but I found their services useful. It costs you nothing to check them out, but you can support me this way. They are basically a very good unified job search platform, where you apply once (with salary request, and conditions), then employers can make offers. It is hidden from your current employer, absolutely free to use and if you get hired, you even get a (not insignificant) cash bonus. I encourage you to check them out. Here is the link.

Leave a Reply