Writing a StarCraft Bot in F#

Hi, I’m Chris Smith. You might know me as the world famous author of Programming F#, but outside of my pseudo-tech-celebrity status I’m a real person. I’m a part time movie critic, avid skier, and a gamer. My passions are deep wide and one thing that has always excited me is the notion of programs playing computer games. Seriously, I know it’s strange. Anyways, I was overjoyed when I heard about the 2010 Conference on Artificial Intelligence and Interactive Digital Entertainment StarCraft Competition.

Artificial intelligence, programming, and StarCraft. It’s like peanut butter and jelly, only with Zerglets.

The day the contest was announced I worked feverishly on creating a StarCraft bot in F#. F# lends itself quite naturally to AI, asynchronous programming, and things which are awesome. However, I never blogged about this earlier on ethical grounds. Namely, that the API used to interact with Broodwar blatantly violates StarCraft’s EULA. Think about it, APIs to give programmatic access to computer games are dangerous. Encouraging bot development goes hand in hand with hacking, exploits, and ruining the game for everyone. While my own motives for modifying StarCraft are to write AI bots for my own use, I absolutely respect Blizzard’s wishes to prevent the reverse engineering of their software.

So, as a representative of Microsoft and one who respects license agreements, I kept my excitement quiet. That is, until the news came that Blizzard’s legal department gave the go ahead to the AIIDE. So, finally, let me introduce my StarCraft Bot in F#. (Which is really just a port of the Java Proxy Bot.)

Getting Setup

1. Buy and Install StarCraft

The first order of business is to install StarCraft and the BroodWar expansion. You can pick it up for $15 at Blizzard's Online Store.

Be sure to install StarCraft into an easily accessible folder, like “E:\StarCraft”. Otherwise copying StarCraft bot files into the admin-only Program Files folder will be a huge pain.

2. Download and Install Chaos Launcher

Chaos Launcher is a tool which customizes how StarCraft loads and executes. Since the game originally came out in March, 1998 I won’t fault it for not rendering in a windowed-mode. However, for bot-development this is critical.

First, unzip the file into a folder, such as “E:\StarCraft\Chaos Launcher\”. Then, to test things out simply run ChaosLauncher.exe. Check W-Mode plugin and click Start. Now you can play StarCraft in a window!

image

3. Install the BWAPI

BWAPI is an open source API for interacting with StarCraft: BroodWar and is the API we will use to control our AI bot.

Download BWAPI_Beta_2.6.1.zip and unzip it into a folder, such as “E:\StarCraft\BWLAPI_Beta_2.6.1\”. Note however, that you still have more registration to do. From the steps listed in the README file:

1. Copy the contents of the ChaosLauncher folder to your Chaos Launcher folder (“E:\StarCraft\Chaos Launcher"\”)
2. Copy the contents of the StarCraft folder to your StarCraft folder (“E:\StarCraft\”)
3. Copy the contents of the Windows folder to your System folder (“C:\Windows\”)

Introducing StarCraftBot 9K

We now have all the pieces in place, but creating an AI-bot for a program not meant to be extended will be tricky. StarCraft Bot 9K has the following architecture, which follows the footsteps of the Java ProxyBot available on the AIIDE Website.

image  
1. You use Chaos Launcher to launch StarCraft
2. When a StarCraft game starts, the BWAPI code executes on each game tick
3. On each tick the StarCraftConnector project (C++) broadcasts all public game data
4. The StarCraftBot9K Client (C#) will listen for those events
5. And finally StarCraftBot9K (F#) will do decision making

4. Download StarCraftBot9K

At the very bottom of this blog post is a .zip file with all the source code. Download that and unzip it into a folder, such as “E:\StarCraftBot9K\”.

5. Install the StarCraftConnector

One output of building the StarCraftBot9K soolution is StarCraftConnector.dll, which is the C++ library built on top of BWAPI that broadcasts game state to a socket. Later a C# application will listen to that data and send messages back to the StarCraft game.

To ‘install’ StarCraftConnector, copy it to “E:\StarCraft\bwapi-data\AI\”. This is the folder that you copied when installing BWAPI.

Next, edit “E:\StarCraft\bwapi-data\bwapi.ini” and set the ai_dll field to “bwapi-data\AI\StarCraftConnector.dll”.

image

6. Run StarCraftBot9K

Finally, you are ready to unleash StarCraftBot9K! Simply start run the application. It will begin listening to the socket until StarCraftConnector begins broadcasting.

image

 

7. Launch StarCraft

Next, run Chaos Launcher again, this time checking the “BWAPI Injector” module. Note that you must run it as an administrator.

image

You also need to check “BWAPI Injector” so that BWAPI get’s loaded with StarCraft.

image

When StarCraft starts everything will act as normal, BWAPI and the StarCraftConnector only come into play when starting a Custom Game.

image

8. Give StarCraftBot 9K Control

Once the game begins and you are connected you should see something like this. It is a mirroring of the internal StarCraft game state onto a simple WinForms app. There still is a lot of scaffolding to add, such as building up the object model to send proper commands to StarCraft.

image

When you check “Economy AI Module”, an F# asynchronous workflow will start up and monitor StarCraft trying to build up your basic economy. (Building SCVs, Drones, or Probes, and sending them to mine.)

 /// AI module for managing your economy. Building drones, vespen geysers, etc.
let private getEconomyAI (mediator : GameMediator) =
    async {

        while true do

            let currentState = mediator.CurrentGameState

            // Build workers with all available cash
            if currentState.SupplyUsed < currentState.SupplyTotal &&
               currentState.Minerals >= 50 &&
               currentState.CanProduce.[ int (getWorkerType(g_GameMetadata.PlayerRace)) ] then
               
                // BUG: Builds a worker at the first command center we find, this might not be the best one.
                // Ideally we'd have some higher level notion of a 'base' with a 'status' such as 'active' or 'out of resources'.
                let cmdCenter = 
                    currentState.Units 
                    |> Seq.filter (fun unit -> unit.Player = g_GameMetadata.PlayerID)
                    |> Seq.tryFind (fun unit -> isCommandCenter (enum<UnitID> unit.TypeID))
                    
                if Option.isSome cmdCenter then
                    let cmdCenterID = cmdCenter |> Option.get |> (fun unit -> unit.ID)
                    let cmd = TrainUnit(cmdCenterID, int (getWorkerType(g_GameMetadata.PlayerRace)))
                    mediator.SendCommand(cmd)

            // Send idle workers to closest mineral patch
            for scunit in currentState.Units do
                // If you own the unit, it's a worker, and it's sitting there then
                // send it to the closest mineral patch.
                if scunit.Player = mediator.GameMetadata.PlayerID &&
                   isWorker scunit &&
                   scunit.OrderID = int (UnitOrder.PlayerGuard) then
                       
                    let orderID = scunit.OrderID

                    let scunitLoc = { X = scunit.XPos; Y = scunit.YPos }

                    let mineralPatches = 
                        currentState.Units
                        |> Seq.filter(fun unit -> unit.TypeID = int UnitID.MineralField)
                        |> Seq.map(fun minPatch -> let patchLoc = { X = minPatch.XPos; Y = minPatch.YPos }
                                                   scunitLoc.FlyingDistanceTo(patchLoc), minPatch)
                        |> Seq.sortBy(fun (dist, minPatch) -> dist)
                        
                    if mineralPatches <> Seq.empty then
                        let targetMinPatch = mineralPatches |> Seq.head |> snd
                        let cmd = RightClickUnit(scunit.ID, targetMinPatch.ID)
                        mediator.SendCommand(cmd)

            // Sleep and check again later. If this agent gets canceled it will be right here.
            do! Async.Sleep(100)
        }

The AI isn’t anything earth shattering yet. But F#’s asynchronous workflows, the reactive framework, and  first class events should make for a very powerful and elegant model on top of asynchronous operations happening in game.

Consider asking a unit such as a goliath to attack an enemy. Lots can happen! The goliath can kill the other unit, that unit can run away, the goliath can be killed, and so on. Even worse is that you won’t know the result of that action until several seconds later. F#’s asynchronous workflows provide a clean way to wrap those asynchronous actions.

 let! result = tryEngage enemyUnit

match result with
| MarineWasKilled -> printfn "Sad."
| UnitWasKilled   -> printfn "Horray! Now go find a medic to heal"
| UnitRanAway     -> printfn "Should you chanse? Maybe..."

The tryEngage function could then simply be a union of several events (marine.OnKilled, unit.OnKilled, and new event when the unit is too far away). This concept may be difficult to grasp right now, I’ll try to post a follow up illustrating this concept later.

I’ve attached the source code. While it could stand to benefit from a lot of polishing it should be very straight forward. However, note that this post comes with not one but two disclaimers! Consider yourself blessed.

All code samples are provided "AS IS" without warranty of any kind, either express or implied, including but not limited to the implied warranties of merchantability and/or fitness for a particular purpose. So in other words, if using the BWAPI somehow gets you banned from Battle.net or causes damage to your system, don’t expect any sympathy.

As you can tell there are a lot of moving parts here. I’m glad to provide technical support, but if you venture off the beaten path to don’t expect me to know why <random thing> isn’t working or how StarCraft is implemented.

image

StarCraftBot9K - 2010-03-18.zip