Lightning AI
Lightning is a StarCraft: Brood War AI

Go to GitHub Java C++ AI

Lightning AI is a finite state automata designed to play Blizzard's iconic real-time strategy game StarCraft: Brood War. It is built to take advantage of the computer's inhumanly fast speed and ability to divide its attention. It makes use of the third-party libraries BWAPI and BWMirror to interface with the game as a player.

Strategy game AI has been in development for a long time. Perhaps most famously, Deep Blue was known for defeating chess champion Garry Kasparov in 1997. Deep Blue used opening and closing tables extensively, but achieved its strength mainly through brute force searching of the game space. However, modern video games - including the real-time strategy genre - involve continuous positions and simulatenous gameplay, making any sort of tree traversal search infeasible. This calls for a vastly different AI structure to play the modern strategy game.

Design Principles

Non-Cheating

Because of the challenges involved in making a strong AI player, most developers tend to only ship a mediocre opponent and rely on giving the AI information and resources that are normally unavailable to a player in order to present a challenge. For example, the AI opponent may be able to see into the fog of war, or receive free resources and units.

BWAPI restricts the information that the AI can obtain to the same information that would be available to a player in the game. The AI may not see units and buildings that have passed into the fog of war. BWAPI also restricts the commands that may be issued by the AI. The AI may only build units and otherwise issue commands that a player would be allowed to.

Competative

The primary purpose of Lightning is to compete against other AIs of the same type. Tournaments hosted by various organizations such AIIDE pit AI against AI. The goal of Lightning is to achieve a high ranking on the tournament ladder.

Adaptive

AIs also tend to play against human opponents. Here, Lightning should be able to hold its own against poor to intermediate opponents. A large complication in playing against human opponents is their unpredictability. Lightning should be able to adapt to changing situations, including some anti-computer tactics employed by human opponents.

The AI's strategy is implemented using a Deterministic Finite State Machine (DFSM), called the BotState. The BotState can adjust to changing game conditions and determine what high-level strategy is optimal for the AI to take at any given time.

Modular

The source code of the AI should be built to accomodate a number of strategies. Adjustments to the core strategy and details alike should be easy to make.

The core of Lightning is the BotState. This stores the AI's current strategy and can change based on input parameters. The remainder of AI supports the high level strategy determined by the BotState. It is implemented using a set of interdependent pluggable modules called Managers.
Name Description
BaseManager Keeps track of which bases are occupied by who
BuildManager Handles determining what the AI wants to build and determines when it can
DebugManager Records and displays debug information
MicroManager Handles individual movements of units
PathingManager Generates routes for units with given parameters

These managers are all implemented as static classes so that they can be accessed from anywhere inside the program at any time. This avoids the earlier issue of passing around various managers, simplifying initialization and reducing the number of member variables inside each class.

Features

Java 8 Idioms

Much of the source code is written in the new Java 8 fluent functional-chaining style, allowing for code to be both efficiently parallelized and fluently read. For example, the following code goes through the unitQueue and finds a unit to train each unit in the queue.

// Auto train
BuildManager.unitQueue.stream()
    .forEach(
        toTrain -> GameHandler.getMyUnits().stream()
            .filter(u -> u.getType() == toTrain.whatBuilds().first)
            .filter(u -> !u.isTraining())
            .findFirst()
            .ifPresent(u -> u.train(toTrain))
    );

Complete Documentation

Internal interfaces are fully documented with detailed Javadocs. In Eclipse, these Javadocs can be accessed by simply hovering the cursor over the class or method.

/**
 * Process a command meant for a {@link #DebugModule} and search through
 * {@link #debugModules} for the correct one to forward it to.
 * 
 * @param command
 *            The command being parsed, split by whitespace.
 * @throws InvalidCommandException
 *             if the command cannot be parsed
 */
        

The Javadoc source code.

Javadoc of DebugModule

The Javadoc as parsed by Eclipse.

Debug Manager

A powerful, yet simple to implement debug manager allows the programmer to quickly and easily see what the AI is thinking at any given time. The debugger is modular so different sets of information can be quickly displayed or hidden at a keystroke.

Writing a module for the debugger is as simple as knowing what you want to show. The main module resides in a static class called DebugManager. To display information through it, simply register a fire-and-forget DebugModule, as below.

createDebugModule("test")
    .addCommand(
        null,
        (c) -> GameHandler.sendText("Type \"/test help \" for this page.")
    )
    .addAlias("help")
    .addCommand(
        "secret",
        (c) -> GameHandler.sendText("The secret command " + c + "is activating... Shh!")
    )
    .setDraw(() -> DrawEngine.drawTextMap(320, 240, "Testing..."));

This creates a module that can be toggled with the command /test in the game client.

The internal interface relies heavily on function-chaining to make the declarations fluent. Used in conjunction with Java 8 lambda functions and functional interfaces, the entire interface is very straightforward to read. The registration process was designed specifically so that the declaration would remain in the same lexical scope as the declaring class. This provides access to private and protected variables without having to add clunky public accessor methods, leading to quick and easy debugging.

And you're good to go! The debugger should not interfere with regular operation of the class. It simply serves as an easy way to display the internal state of various parts of the AI

.