Tutorial for creating a StarCraft bot

We will show you all the steps required to create a StarCraft bot, run it on your computer, and have it play on SSCAIT.

Introduction

Your bot will communicate with StarCraft using the Brood War API (BWAPI). BWAPI lets your bot play the game instead of a human player.

We will walk you through making a bot using the Java language on Windows. We'll briefly discuss other languages and operating systems you can use before getting back to the tutorial for Java on Windows.

Other ways to get started

Languages with good support include C++, Scala, Kotlin, and Ruby. Python (PyBrood, TorchCraft), C#, and Rust have BWAPI libraries but are less well-tested. The setup steps are mostly the same for all languages.

StarCraft and BWAPI only run in Windows, but you can develop on other operating systems with some extra steps. You can run games or compile C++ bots using WINE, Docker, or virtual machines. SC-Docker runs BWAPI games on multiple platforms. StardustDevEnvironment is for writing bots in Linux or MacOS. It uses OpenBW which is an alternative version of StarCraft and BWAPI which is cross-platform, but not immediately compatible with bots compiled for StarCraft 1.16.1. StardustDevEnvironment lets you test locally on OpenBW and then compile a Windows binary for competitions like the SSCAI Tournament.

And lastly, if you're interested in writing a bot in C++ on Windows, check out Dave Churchill's C++ bot tutorial. You may still find most of this tutorial helpful, as the Java and C++ interfaces are very similar.

Getting help

If you get stuck in your StarCraft AI journey, check out the Troubleshooting section below, or visit the SSCAIT Discord chat room. The community is very welcoming to new authors and eager to help.

Java bot on Windows: Setup

Video tutorial for some of these installation steps

Creating your bot

Run IntelliJ IDEA. Click "Open" and select the directory where you copied the JBWAPI Java Template. In the file explorer on the left, open jbwapi-java-template → src → main → java → Bot. This is your new empty bot file.

Running your bot

Right click main in the code editor and select "Run".

Running a bot in IDEA by right clicking 'main' and selecting 'Run'

IntelliJ IDEA will compile and run your bot. In a few seconds you should see your Bot's console output saying "Game table mapping not found." That means your bot is successfully running! It's just waiting for you to launch StarCraft so it can start playing.

The console shows that your bot is running

Chaoslauncher is the program you'll use to launch StarCraft with BWAPI enabled. It's in the directory where you installed BWAPI under Chaoslauncher\Chaoslauncher.exe. Run it. Go to the Settings tab and disable 'Warn about missing admin privileges'. In the Plugins tab, enable "BWAPI 4.4.0 Injector [RELEASE]". If you'd like to run StarCraft in a window, enable W-MODE.

Click "Start" to run StarCraft. In StarCraft, click "Single Player" → "Expansion" → Create an ID if you don't have one → Ok → "Play Custom" → Select a map you unzipped into "sscai" earlier. Add a Computer opponent and click "Ok" to start the game!

A screenshot of a StarCraft game showing 'Hello World' text

If your bot connects to the game, its console output will say "Connection successful", and StarCraft should look like the screenshot above. The bot will print "Hello World!" to the screen. You can exit the game now. Everything's set up and you're ready to get coding!

The next time you run the game from Chaoslauncher, consider configuring BWAPI to start the game automatically. In Chaoslauncher, click the "BWAPI 4.4 Injector [RELEASE]" plugin and the "Config" button to open a text editor containing BWAPI.ini. Edit it to set the following values:

  • auto_menu = SINGLE_PLAYER
  • maps = maps\sscai\*.sc?
  • Set race and enemy_race to whatever race you want your bot and enemy to be.

Event Listeners and the API

Throughout this tutorial we will link to the C++ API documentation because it is most descriptive, but the classes and methods are nearly identical in Java.

  • In the source code, you'll find the onFrame() method. That's the implementation of the event listener that's called once on every logical game frame (that's 23.81 times per second on the Fastest game speed setting which most humans use).
    There is also the onStart() listener implementation that's called only once, when the game starts.
    Most of the simple bots only use these two listeners.
  • However, your bot's code can also be hooked to other event listeners and get executed after various other game events. To do that, you can either implement the interface directly, or extend the stub class DefaultBWListener.
  • The API offers other event handlers, described more in the C++ API reference. Some commonly used ones include onUnitCreate, onUnitShow, onUnitMorph, and onUnitDestroy. There's also a reference for which unit transitions trigger which events.

Implementation and Important Classes

The Game class

The Game object, which you aquire bycalling bwClient.getGame(), gives you access to units, players, and other information about the current game. self() returns your Player object (described below) and enemies() returns the Player objects of your opponents. getAllUnits() returns all units visible to you.

The Game also provides you with the ability to print draw text or simple geometry overlaid on StarCraft for debugging your bot. See the collection of methods: draw[Line, Circle, Text, etc.]Screen renders shapes using screen overlay coordinates and draw[...]Map renders on top of the game world, subject to camera position.

StarCraft like most software uses screen coordinates, rather than Cartesian coordinates. The top left corner of the map is (0, 0). Like Cartesian coordinates, X values increase moving right, but Y coordinates increase downwards rather than upwards. This coordinate system also means that angles, as returned by unit.getAngle(), or as used by any trigonometric functions, increase clockwise rather than counter-clockwise.

Game offers some other tools that can assist your debugging:

  • game.enableFlag(1) allows you to issue commands from the UI. Your bot is free to override these but this can allow you to manually train units, cancel buildings, or control units that your bot ignores.
  • game.setLocalSpeed(0) sets the delay between game frames to zero, causing StarCraft to run as fast as possible. In a multiplayer game this is still limited by the speed of your opponents' instances of StarCraft. The argument is a delay, in milliseconds, between game frames. The delay on the "Fastest" speed, used by most humans, is 42. You can also set this delay from the in-game chat by typing "/speed 0"

The Player class

A Player object gives you access to your units, upgrades, and resources. The following lines of code draw some information about the Player on the screen:

Player self = game.self();
game.drawTextScreen(10, 10, "Playing as " + self.getName() + " - " + self.getRace());
game.drawTextScreen(10, 230, "Resources: " + self.minerals() + " minerals,  " + self.gas() + " gas");

The Unit class

Most in-game objects are Units. This includes troops like Zerglings and Overlords, buildings like Gateways, add-ons like Machine Shops, and mineral patches.
Game.getAllUnits() returns all units. Game.self().getUnits() returns only your units. The Unit interface provides information about units, like getType() or getPosition().

BWAPI only reveals information that would be accessible to a human player. someEnemyPlayer.getUnits() will only return units that are presently visible to your bot. If you are holding on to Unit objects for enemy units that were previously visible but are now hidden, calling methods requesting information on them will return empty values. unit.exists() will let you know if a unit object will return valid data.

Enemy units which are cloaked but not in range of friendly detectors are available, but only their owner, type, and position will be accessible. If you want your bot to "remember" information about enemy units that may become hidden later, you may want to record and store information about those enemy units in your own objects.

The Unit object also allows you to issue commands to units:

  • move(...): Moves the unit, if it can move
  • build(...): If your unit is a worker, instructs it to construct a building
  • gather(...): If your unit is a worker, gathers resources (minerals or gas).
  • attack(...): Makes your unit attack an enemy unit, or travel towards a position attacking enemies along the way.
  • train(...): Trains a new unit, if you have adequate resources to do so.
  • upgrade(...): Starts the upgrade in our building (e.g. damage or armor).
  • research(...): Starts researching a specified technology/ability in our building (e.g. Stimpacks or Parasite).
  • rightClick(...): Does the same thing as if you right-clicked on something in the game with this unit selected.

Because units have different purposes, you likely want to issue different kinds of units different commands. This brings us to...

The UnitType class

Calling getType() on a unit returns its UnitType which gives you a lot of additional information about the unit, such as its max health, cost, weapon type, or even build time. JBWAPI defines constants for all unit types, you can find them as static class fields in the UnitType class like UnitType.Terran_SCV. To test, whether a unit is of a particular type, you can compare its type with one of the predefined constants:

if (myUnit.getType() == UnitType.Terran_Command_Center && self.minerals() >= 50) {
	myUnit.train(UnitType.Terran_SCV);
  }

With this, you can iterate over all your units and issue different commands to each one. For example, the following code makes all your Command Centers train new workers and all your Marines attack the top-left corner of the map:

//iterate over my units
for (Unit myUnit : self.getUnits()) {
	//if this is a Command Center, make it train additional worker
	if (myUnit.getType() == UnitType.Terran_Command_Center) {
		myUnit.train(UnitType.Terran_SCV);
	}

	//if this is a Marine, let it attack some position
	if (myUnit.getType() == UnitType.Terran_Marine) {
		myUnit.attack(new Position(0, 0));
	}
}

Some of these commands (such as attacking or constructing new buildings) require you to specify a location as an argument. Let's take a closer look at position types.

Position classes

BWAPI, JBWAPI and StarCraft uses three position concepts:

  • Position: Represents a pixel-precise location. Use unit.getPosition() to get the center position of a unit.
  • WalkPosition: The resolution of StarCraft's terrain collision grid is 8 pixels wide. A WalkPosition is a coordinate representing an 8-by-8 area.
  • TilePosition: The resolution of StarCraft's legal building locations, visibility, and cloaked detection is 32 pixels wide. A TilePosition is a coordinate representing a 32-by-32 area. unit.getTilePosition() returns the tile position containing a unit's top-left corner.

The following code draws the TilePosition and Position of each of your workers right right next to them. The first two arguments specify where on the map to draw this text, with Position-(pixel-)precision (Position.getX() and Position.getY()). Try experimenting with this code yourself.

//iterate over your units
for (Unit myUnit : self.getUnits()) {

	//print TilePosition and Position of my SCVs
	if (myUnit.getType() == UnitType.Terran_SCV) {
		game.drawTextMap(myUnit.getPosition().getX(), myUnit.getPosition().getY(),
			"TilePos: "+myUnit.getTilePosition().toString()+" Pos: "+myUnit.getPosition().toString());
	}
}

In a similar manner, you could to print out the current order each unit is performing or even draw a line to their destinations:

game.drawTextMap(myUnit.getPosition(), myUnit.getOrder().toString());
game.drawLineMap(myUnit.getPosition(), myUnit.getOrderTargetPosition(),  bwapi.Color.Black);

Constructing Buildings

The Game class exposes information about the map's dimensions and which tiles are explored, visible, walkable or buildable. You can use this information to deicde where to place buildings For example, let's take a look at how you can order your workers to build a Supply Depot:

//if we're running out of supply and have enough minerals...
if (self.supplyTotal() - self.supplyUsed() < 8 && self.minerals() >= 100) {
	//iterate over units to find a worker
	for (Unit myUnit : self.getUnits()) {
		if (myUnit.getType() == UnitType.Terran_SCV) {
			//get a nice place to build a supply depot
			TilePosition buildTile =
				getBuildTile(myUnit, UnitType.Terran_Supply_Depot, self.getStartLocation());
			//and, if found, send the worker to build it (and leave others alone - break;)
			if (buildTile != null) {
				myUnit.build(UnitType.Terran_Supply_Depot, buildTile);
			}
			break;
		}
	}
} 

This code checks if we're running out of free supply and have enough minerals for the depot. It finds a worker, then finds a TilePosition near our start location where we can build a Supply Depot and orders the worker to do it. getBuildTile() isn't a function in JBWAPI; here's one way you could implement it:

// Returns a suitable TilePosition to build a given building type near
// specified TilePosition aroundTile, or null if not found. (builder parameter is our worker)
public TilePosition getBuildTile(Unit builder, UnitType buildingType, TilePosition aroundTile) {
	TilePosition ret = null;
	int maxDist = 3;
	int stopDist = 40;

	// Refinery, Assimilator, Extractor
	if (buildingType.isRefinery()) {
		for (Unit n : game.neutral().getUnits()) {
			if ((n.getType() == UnitType.Resource_Vespene_Geyser) &&
				( Math.abs(n.getTilePosition().getX() - aroundTile.getX()) < stopDist ) &&
				( Math.abs(n.getTilePosition().getY() - aroundTile.getY()) < stopDist )) {
        return n.getTilePosition();
      }
		}
	}

	while ((maxDist < stopDist) && (ret == null)) {
		for (int i=aroundTile.getX()-maxDist; i<=aroundTile.getX()+maxDist; i++) {
			for (int j=aroundTile.getY()-maxDist; j<=aroundTile.getY()+maxDist; j++) {
				if (game.canBuildHere(new TilePosition(i,j), buildingType, builder, false)) {
					// units that are blocking the tile
					boolean unitsInWay = false;
					for (Unit u : game.getAllUnits()) {
						if (u.getID() == builder.getID()) continue;
						if ((Math.abs(u.getTilePosition().getX()-i) < 4) && (Math.abs(u.getTilePosition().getY()-j) < 4)) unitsInWay = true;
					}
					if (!unitsInWay) {
						return new TilePosition(i, j);
					}
					// creep for Zerg
					if (buildingType.requiresCreep()) {
						boolean creepMissing = false;
						for (int k=i; k<=i+buildingType.tileWidth(); k++) {
							for (int l=j; l<=j+buildingType.tileHeight(); l++) {
								if (!game.hasCreep(k, l)) creepMissing = true;
								break;
							}
						}
						if (creepMissing) continue;
					}
				}
			}
		}
		maxDist += 2;
	}

	if (ret == null) game.printf("Unable to find suitable build position for "+buildingType.toString());
	return ret;
}

This function searches an increasingly large area (while loop) around aroundTile for TilePositions where game.canBuildHere() returns True. The search starts on 3-tile area and fails (returns null) if no solution is found within 40-tile area around aroundTile. It also skips those tiles, that are blocked by some units and in case of Zerg buildings, it also verifies the creep coverage. Refineries, Assimilators and Extractors are more simple, special cases, since they can only be built on top on Vespene Geysers.

Finding the Enemy Base

Most StarCraft games involve building new bases next to clusters of minerals and usually another Vespene Geyser. Most maps provide several ideal locations for this. The Game object exposes the possible starting locations with getStartLocations(). To find other good places to put bases (or where your opponent may put them), you can use a terrain analyzer called BWEM (Brood War Easy Map). JBWAPI ships with a Java port of BWEM; there's also a modern C++ version.

This code loads BWEM, gets BWEM's recommended base locations, and draws them on the map:

private BWClient bwClient;
private Game game;
private BWEM bwem; // Stores BWEM data
void onStart() {
game = bwClient.getGame()
// Load BWEM and analyze the map
bwem = new BWEM(game);
bwem.initialize();
}
void onFrame() {
  for (final Base b : bwem.getMap().getBases()) {
    game.drawMap(
      b.getLocation.toPosition(),
      b.getLocation.toPosition().add(new Position(31, 31)),
      Color.Blue);
  }
}

Remembering Enemy Buildings

There comes a time in the game when you want to attack your opponent. With the code above, you should be able to find the enemy by sending some units to all BaseLocations. When you discover some enemy buildings (when you see them, the game.enemy().getUnits() function returns non-empty set), you should remember their location, so that you don't need to look for them in future. Prepare some kind of register for enemy building positions and always keep it up to date. For example, you can declare the following HashSet that will hold all the positions where we saw an enemy building:
private HashSet enemyBuildingMemory = new HashSet();

Then, somewhere in the onFrame() function, you need to keep that HashSet up to date. Take a look at the following example code. First part of it adds currently visible enemy units to memory HashSet, and the second part removes remembered positions if there are no longer any buildings on them (after they've been destroyed).

//always loop over all currently visible enemy units (even though this set is usually empty)
for (Unit u : game.enemy().getUnits()) {
	//if this unit is in fact a building
	if (u.getType().isBuilding()) {
		//check if we have it's position in memory and add it if we don't
		if (!enemyBuildingMemory.contains(u.getPosition())) enemyBuildingMemory.add(u.getPosition());
	}
}

//loop over all the positions that we remember
for (Position p : enemyBuildingMemory) {
	// compute the TilePosition corresponding to our remembered Position p
	TilePosition tileCorrespondingToP = new TilePosition(p.getX()/32 , p.getY()/32);

	//if that tile is currently visible to us...
	if (game.isVisible(tileCorrespondingToP)) {

		//loop over all the visible enemy buildings and find out if at least
		//one of them is still at that remembered position
		boolean buildingStillThere = false;
		for (Unit u : game.enemy().getUnits()) {
			if ((u.getType().isBuilding()) && (u.getPosition().equals(p))) {
				buildingStillThere = true;
				break;
			}
		}

		//if there is no more any building, remove that position from our memory
		if (buildingStillThere == false) {
			enemyBuildingMemory.remove(p);
			break;
		}
	}
}

In general, you may want to remember much more information in addition to building positions. It's all up to you.

And that's it! You should now be able to order your workers to gather resources, order buildings to train new units, construct more buildings, find and remember enemy buildings and send your units to attack them. This covers the basics and should be enough to win some games. There is much, much more you can do to beat your opponents in StarCraft, but we leave that up to you.

Submitting your bot

To submit your bot to the SSCAI Tournament, go to the Log In & Submit Bots subpage. You should upload a zip archive containing these 3 things:

  1. README file (TXT or PDF) with the description of your bot.
  2. Compiled bot. Either a .JAR file if it's coded in Java (read the instructions below), or .dll file if you used C++.

If for some reason you decide to disable your bot once submitted, upload an empty ZIP archive as your bot's binaries.

Troubleshooting

My bot works fine locally but crashes or timeouts while playing on the SSCAIT stream.

You should make sure that the release version of your bot that you upload to SSCAIT doesn't try to print to STDOUT (the "console", often via "sys.out.println()" in Java or "printf" or "std::cout" in C++).

How can I build a standalone version of my bot as a single runnable jar file?

In IntelliJ IDEA, go to View -> Tool Windows -> Maven, then right click "package" and then "Run Maven Build". This will generate target/jbwapi-template-1.0-SNAPSHOT-jar-with-dependencies.jar, which is a runnable JAR file. You can verify that this JAR bot runs from the Windows command line (Start → Run → type in 'cmd'), using the following command: java -jar jbwapi-template-1.0-SNAPSHOT-jar-with-dependencies.jar
Then you can run StarCraft via Chaoslauncher, as usual, and verify that the bot works. Creating a runnable jar with Run Maven Build on 'package'

My bot can't connect to the game.

On some systems, the administrator privileges might be needed for the bot to run. Try running the bot (Eclipse) or Chaoslauncher (or both) as Administrator.

Still stuck?

The best place to ask questions is the SSCAIT Discord chat room. You can also ask in questions in the BWAPI Facebook group.