|
The signups for the 2015 AIIDE StarCraft AI Competition were announced this week, which has been good motivation for me to get back into working on my AI. I competed last year and my AI came 9th out of 18 competitors; with a win rate of just over 50%. I was fairly happy with that as result for my first attempt, but I'm hoping to do better this year.
I was previously working on my AI mostly on my desktop at home, but now I'm in Japan I only have this crappy laptop so I've had to install a bunch of stuff to be able to continue. I thought that since I've been having to set everything up again, I might try writing a guide for any one else who is interested in getting into SC:BW AI programming.
I think it's worth noting that I'm not a professional programmer, and I have fairly limited formal education (1 year). If you look at the code for my AI, a lot of it is a horrible mess, this is partly because I was learning C++ while working on it, and partly because I'm still not that great at it. Despite that, I think I'm qualified to at least explain the basics of how to start working on a BW AI system. The AI project last year was for my MSc dissertation, and I got a pretty good grade, so it can't have been completely horrible lol. Anyway, I apologise in advance if I give out any incorrect information or if any of my example code is ugly as fuck.
Also, all credit for the ExampleAIModule code that we'll be looking at in this guide goes to heinermann, the guy behind BWAPI.
Background: why develop AI for SC:BW? + Show Spoiler +The best AI systems for games like Chess are really good, and can compete with the best players in the world. The best AI systems for StarCraft are complete garbage in comparison, and can be defeated by even mediocre players. This is because StarCraft is a game with a much higher level of complexity. In Chess, there are a huge number of possible moves that can be made, but in StarCraft the number is much higher. In comparison to a StarCraft map, a Chess board is fairly small with a limited set of pieces and locations that pieces can occupy. A StarCraft map can have hundreds of units, and each of these units can occupy thousands of different locations on the map, and also have multiple abilities and statuses. This is before you even take into account structures, resource gathering or unit production. Another factor which makes StarCraft AI programming difficult are the real-time constraints. To use a Chess AI as an example again; in Chess, players take turns, so the AI has all the time it needs to calculate possible moves and weigh up which is most effective before ending its turn. In contrast, StarCraft is played in real-time, which means that the AI doesn't have time to wait and calculate all the possible moves. Things like tree searches would take ages in a StarCraft game because of the previously mentioned enormous complexity, and the fact that the game is played in real-time means that that available calculation is quite small (in the AIIDE competition, an AI which regularly takes longer than 55ms to finish a frame will be disqualified from that game). So basically SC:BW creates an interesting set of challenges for AI programmers. It's also a great game, and testing mostly involves watching your AI play, which is quite fun. If you want a more detailed, and better written account of the challenges and previously explored solutions in StarCraft AI programming then I would recommend this paper. It's a pretty good introduction because it talks about the various challenges facing SC AI programmers and talks about some of the more successful AI systems and their architectures.
Before getting started: required skills + Show Spoiler + While you don't need to go in as some kind of programming wizard or AI expert, I would recommend that you have some kind of programming experience or training before getting involved with a project like this. Before I started working on my AI, I had never touched C++ or Visual Studio before and had no idea about anything to do with AI, so it's definitely possible to learn as you go. However I was studying Computer Science at University at the time, and had already taken courses in C and Object Oriented Programming so picking up C++ wasn't particularly challenging.
If you've never done any programming before, but you think you might be interested, I would recommend following some tutorials or online courses first. Try to familiarise yourself with basic programming concepts and get some experience writing simple programs before you start on this.
However, if you already have some vaguely relevant experience then there's no reason you can't do this. Just have a go; I think it's really fun.
Getting started: installation guide + Show Spoiler +I've only ever done this on Windows 7 and 8. I don't know what works and doesn't work on other operating systems. If you're on Windows 7 or 8 (or possibly others) and you follow these steps then I guess it should work. If not, then maybe write a comment and I may or may not be able to help. All the versions of BWAPI that I have used come with an ExampleAIModule which is a good way to test that you've installed everything correctly. After installing stuff, follow the next steps to compile and run the ExampleAIModule; if it works then you know you're all set up to start coding. Note: If you're using a different version of BWAPI or Visual Studio to me then things might be in slightly different places. Downloading and installing: 1. If you haven't got it already, install SC:BW and update it to version 1.16.1. 2. Download the latest version of the BroodWar Application Programming Interface (BWAPI) (as of writing the latest version is BWAPI 4.1.1 Beta) from here, and install it. You will also need ChaosLauncher, but that should come with BWAPI when you install it. 3. Download the latest version of Visual Studio C++. I downloaded Visual Studio Community 2013 from here but other versions might work too. VS always comes with a million extra things, and you can probably untick some of the boxes if you don't want it all; as long as you get the C++ stuff. Compiling and running the Example AI 4. Go to the directory you installed BWAPI. Inside there should be a folder called ExampleAIModule. Inside this there should be a VC++ Project file called ExampleAIModule.vcxproj; open this in Visual Studio. Note: ExampleAIModule; NOT ExampleAIClient or ExampleTournamentModule. 5. When Visual Studio has finished dicking around and is ready to use, find solution configuration and change it from 'Debug' to 'Release'. It's here. 6. In the Solution Explorer window, right click on ExampleAIModule and click 'Build'. Like this. 7. Check Output window to make sure that the project compiled correctly. It should say "Build: 1 succeeded, 0 failed". If there are any errors then you won't be able to go on to the next steps until they are resolved. It should look something like this. 8. Go back to your BWAPI directory (up one level from the ExampleAIModule folder) and find the folder named 'Release'. Inside you should find a file named ExampleAIModule.dll; copy this file. Note: compiling will also create another folder called Release inside the ExampleAIModule folder; this is the wrong folder; you won't find the dll file here. Look inside the Release folder in your BWAPI directory instead. 9. Find where you have StarCraft installed. Inside this directory you hopefully have a folder called bwapi-data, inside this is another folder called AI. Go to Starcraft/bwapi-data/AI/ and paste the newly created ExampleAIModule.dll inside. Note: I suppose if these folders aren't there, you can probably create them. 10. Now you need to run ChaosLauncher. Go back to your BWAPI directory. Inside, there should be a folder called ChaosLauncher; inside this should be ChaosLauncher.exe. Make sure to right click and run this as Administrator or you might get some stupid error message. 11. In ChaosLauncher, make sure 'BWAPI Injector (1.16.1) RELEASE' is ticked. 12. Select the 'BWAPI Injector (1.16.1) RELEASE' option and click on the button that says 'Config'; this should open a text file. Near the top of the text file should be a line that says "ai = bwapi-data/AI/ExampleAIModule.dll". If ai is = to something other than "bwapi-data/AI/ExampleAIModule.dll" then the AI won't run; so make sure it's typed in correctly (but it should be there by default). You can now close Config if you want. ChaosLauncher. 13. Click the Start button in ChaosLauncher. This should load SC:BW. Now navigate through the menus to start a single player custom game. Note: this menu screen navigation can be automated from the config file so that clicking Start on ChaosLauncher takes you directly to a game; this isn't necessary though so I haven't put it in the guide. 14. When the game starts, the AI should hopefully immediately start doing it's thing (sometime's it lags a bit at the start, especially the first time). If it says something like 'AI module failed to load' and/or nothing is happening then something has gone wrong. It should look like this. The ExampleAIModule from the version of BWAPI I have downloaded automatically sends it's workers to mine and builds a new worker whenever it's main is idle, but doesn't do much else. If you load into a game and the workers start mining and a new one is being constructed then it's worked. Congratulations, you can now start working on your own AI!
Getting started with BWAPI + Show Spoiler +Now that we know that everything has been set up correctly, let's take a look at the ExampleAIModule's code so we can learn how it works. Firstly we should arm ourselves with the BWAPI Documentation. You'll need to use it a lot at first to find the things you're looking for. Now open ExampleAIModule in Visual Studio and have a look. There should be 3 files: Dll.cpp, ExampleAIModule.cpp and ExampleAIModule.h. I'll be honest, I don't understand the code in Dll.cpp, and I've never modified it so let's just leave that one alone; it's probably something to do with turning your code into a dll file. Lets have a look at the header file, ExampleAIModule.h: ExampleAIModule.h code + Show Spoiler + #pragma once #include <BWAPI.h>
// Remember not to use "Broodwar" in any global class constructor!
class ExampleAIModule : public BWAPI::AIModule { public: // Virtual functions for callbacks, leave these as they are. virtual void onStart(); virtual void onEnd(bool isWinner); virtual void onFrame(); virtual void onSendText(std::string text); virtual void onReceiveText(BWAPI::Player player, std::string text); virtual void onPlayerLeft(BWAPI::Player player); virtual void onNukeDetect(BWAPI::Position target); virtual void onUnitDiscover(BWAPI::Unit unit); virtual void onUnitEvade(BWAPI::Unit unit); virtual void onUnitShow(BWAPI::Unit unit); virtual void onUnitHide(BWAPI::Unit unit); virtual void onUnitCreate(BWAPI::Unit unit); virtual void onUnitDestroy(BWAPI::Unit unit); virtual void onUnitMorph(BWAPI::Unit unit); virtual void onUnitRenegade(BWAPI::Unit unit); virtual void onSaveGame(std::string gameName); virtual void onUnitComplete(BWAPI::Unit unit); // Everything below this line is safe to modify.
};
As you can see, there's a class with a bunch of methods called onSomething(). These are called whenever the in-game event or condition that they represent occurs. For example, onNukeDetect() is called when a Nuke is detected. They all have pretty self-explanatory names to be honest so I won't go into detail explaining them all. If you are confused about one, then you can always check the BWAPI documentation. The ExampleAIModule doesn't do much with most of these methods; the main ones that it uses are onStart() and onFrame(). As you might have guessed, onStart() is called at the start of the game. onFrame() is called once per in-game frame (24 times per second in a game on the fastest setting). As was noted in the installation instructions, the ExampleAIModule simply builds workers and sends idle workers to mine. Let's take a look in ExampleAIModule.cpp to see how it does that. ExampleAIModule.onStart() code + Show Spoiler + void ExampleAIModule::onStart() { // Hello World! Broodwar->sendText("Hello world!");
// Print the map name. // BWAPI returns std::string when retrieving a string, don't forget to add .c_str() when printing! Broodwar << "The map is " << Broodwar->mapName() << "!" << std::endl;
// Enable the UserInput flag, which allows us to control the bot and type messages. Broodwar->enableFlag(Flag::UserInput);
// Uncomment the following line and the bot will know about everything through the fog of war (cheat). //Broodwar->enableFlag(Flag::CompleteMapInformation);
// Set the command optimization level so that common commands can be grouped // and reduce the bot's APM (Actions Per Minute). Broodwar->setCommandOptimizationLevel(2);
// Check if this is a replay if ( Broodwar->isReplay() ) {
// Announce the players in the replay Broodwar << "The following players are in this replay:" << std::endl; // Iterate all the players in the game using a std:: iterator Playerset players = Broodwar->getPlayers(); for(auto p : players) { // Only print the player if they are not an observer if ( !p->isObserver() ) Broodwar << p->getName() << ", playing as " << p->getRace() << std::endl; }
} else // if this is not a replay { // Retrieve you and your enemy's races. enemy() will just return the first enemy. // If you wish to deal with multiple enemies then you must use enemies(). if ( Broodwar->enemy() ) // First make sure there is an enemy Broodwar << "The matchup is " << Broodwar->self()->getRace() << " vs " << Broodwar->enemy()->getRace() << std::endl; }
}
Ok so there's a bunch of stuff here in the onStart() method, with some nice comments so I don't really need to explain it. At the start there is this line: Broodwar->sendText("Hello world!"); This just prints "Hello world!" in the in-game chat. You can use Broodwar->sendText() to write whatever you want. This is not much use unless you're planning on incorporating a "from?" rush into your AI's strategy. There's also a few flags set here, for example: //Broodwar->enableFlag(Flag::CompleteMapInformation); This one is commented out; this means that the AI does not have complete map information. If you enable it then the AI will basically get a maphack. If you want to compete in any of the AI tournaments then keep this flag turned off. The other flag is UserInput; this is turned on; it just means that you are able to perform actions even when the AI is playing. This can be useful if your AI gets stuck doing something while you are trying to test something specific; you can take control and get it to the point that you want it to be at. But obviously if you are going to enter your AI in an AI tournament, you aren't allowed to play too. Most of the rest of this method is just printing information about the game. It's a pretty useful one though, for doing all the stuff that you want done once at the start of the game. Next we'll look at the most important method; onFrame(). It's kinda big so pasting the whole thing probably isn't much help but here it is anyway for reference. ExampleAIModule.onFrame() code + Show Spoiler + void ExampleAIModule::onFrame() { // Called once every game frame
// Display the game frame rate as text in the upper left area of the screen Broodwar->drawTextScreen(200, 0, "FPS: %d", Broodwar->getFPS() ); Broodwar->drawTextScreen(200, 20, "Average FPS: %f", Broodwar->getAverageFPS() );
// Return if the game is a replay or is paused if ( Broodwar->isReplay() || Broodwar->isPaused() || !Broodwar->self() ) return;
// Prevent spamming by only running our onFrame once every number of latency frames. // Latency frames are the number of frames before commands are processed. if ( Broodwar->getFrameCount() % Broodwar->getLatencyFrames() != 0 ) return;
// Iterate through all the units that we own for (auto &u : Broodwar->self()->getUnits()) { // Ignore the unit if it no longer exists // Make sure to include this block when handling any Unit pointer! if ( !u->exists() ) continue;
// Ignore the unit if it has one of the following status ailments if ( u->isLockedDown() || u->isMaelstrommed() || u->isStasised() ) continue;
// Ignore the unit if it is in one of the following states if ( u->isLoaded() || !u->isPowered() || u->isStuck() ) continue;
// Ignore the unit if it is incomplete or busy constructing if ( !u->isCompleted() || u->isConstructing() ) continue;
// Finally make the unit do some stuff!
// If the unit is a worker unit if ( u->getType().isWorker() ) { // if our worker is idle if ( u->isIdle() ) { // Order workers carrying a resource to return them to the center, // otherwise find a mineral patch to harvest. if ( u->isCarryingGas() || u->isCarryingMinerals() ) { u->returnCargo(); } else if ( !u->getPowerUp() ) // The worker cannot harvest anything if it { // is carrying a powerup such as a flag // Harvest from the nearest mineral patch or gas refinery if ( !u->gather( u->getClosestUnit( IsMineralField || IsRefinery )) ) { // If the call fails, then print the last error message Broodwar << Broodwar->getLastError() << std::endl; }
} // closure: has no powerup } // closure: if idle
} else if ( u->getType().isResourceDepot() ) // A resource depot is a Command Center, Nexus, or Hatchery {
// Order the depot to construct more workers! But only when it is idle. if ( u->isIdle() && !u->train(u->getType().getRace().getWorker()) ) { // If that fails, draw the error at the location so that you can visibly see what went wrong! // However, drawing the error once will only appear for a single frame // so create an event that keeps it on the screen for some frames Position pos = u->getPosition(); Error lastErr = Broodwar->getLastError(); Broodwar->registerEvent([pos,lastErr](Game*){ Broodwar->drawTextMap(pos, "%c%s", Text::White, lastErr.c_str()); }, // action nullptr, // condition Broodwar->getLatencyFrames()); // frames to run
// Retrieve the supply provider type in the case that we have run out of supplies UnitType supplyProviderType = u->getType().getRace().getSupplyProvider(); static int lastChecked = 0;
// If we are supply blocked and haven't tried constructing more recently if ( lastErr == Errors::Insufficient_Supply && lastChecked + 400 < Broodwar->getFrameCount() && Broodwar->self()->incompleteUnitCount(supplyProviderType) == 0 ) { lastChecked = Broodwar->getFrameCount();
// Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned); // If a unit was found if ( supplyBuilder ) { if ( supplyProviderType.isBuilding() ) { TilePosition targetBuildLocation = Broodwar->getBuildLocation(supplyProviderType, supplyBuilder->getTilePosition()); if ( targetBuildLocation ) { // Register an event that draws the target build location Broodwar->registerEvent([targetBuildLocation,supplyProviderType](Game*) { Broodwar->drawBoxMap( Position(targetBuildLocation), Position(targetBuildLocation + supplyProviderType.tileSize()), Colors::Blue); }, nullptr, // condition supplyProviderType.buildTime() + 100 ); // frames to run
// Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation ); } } else { // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType ); } } // closure: supplyBuilder is valid } // closure: insufficient supply } // closure: failed to train idle unit
}
} // closure: unit iterator }
As I mentioned before, onFrame() is called every frame(). The majority of the code in this method is contained within this loop: // Iterate through all the units that we own for (auto &u : Broodwar->self()->getUnits()) { //stuff }
So this means that each frame, the AI will go through each of the units (by units we mean all the things under the player's control, including structures) that it owns one at a time and do something. The first few lines inside this for loop are just checks to make sure that the unit we are trying to manipulate actually exists and isn't in some kind of status that makes it unusable. After that, we come to this section: // If the unit is a worker unit if ( u->getType().isWorker() ) { // if our worker is idle if ( u->isIdle() ) { // Order workers carrying a resource to return them to the center, // otherwise find a mineral patch to harvest. if ( u->isCarryingGas() || u->isCarryingMinerals() ) { u->returnCargo(); } else if ( !u->getPowerUp() ) // The worker cannot harvest anything if it { // is carrying a powerup such as a flag // Harvest from the nearest mineral patch or gas refinery if ( !u->gather( u->getClosestUnit( IsMineralField || IsRefinery )) ) { // If the call fails, then print the last error message Broodwar << Broodwar->getLastError() << std::endl; }
} // closure: has no powerup } // closure: if idle
}
This is the section that makes the workers automatically gather resources. It goes through each unit and, if it is a worker, checks whether it is idle. If it finds an idle worker, and that worker is carrying minerals or gas, then it tells them to return those resources to a resource depot (CC/Nexus/Hatchery). If the idle worker isn't carrying anything then the nearest resource patch is found and the worker is commanded to gather from it. Remember this is called every frame, which means that the AI should never have an idle worker for more than 1 frame, or roughly 1/24th of a second. Next, we have the code that automatically builds additional workers: else if ( u->getType().isResourceDepot() ) // A resource depot is a Command Center, Nexus, or Hatchery {
// Order the depot to construct more workers! But only when it is idle. if ( u->isIdle() && !u->train(u->getType().getRace().getWorker()) ) { //do stuff } }
This is still contained within the same for loop as the code that checked for idle workers. But this time we are checking if the unit is a resource depot instead. Resource depot is the general word used for Command Centers, Nexuses, Hatcheries, Lairs and Hives. Notice how all the code here is written using non-race-specific terms like 'worker' and 'resource depot'; this means that it will work equally well for each race. Anyway, as you can see, if we find a resource depot, then we check if it is idle (not training a unit or researching a tech). If it is idle, then we tell it to train a worker. u->train(u->getType().getRace().getWorker())
This is the part that trains the worker. However, it is contained within a condition so that if it fails, an error can be drawn so that we can check what the problem was. // If that fails, draw the error at the location so that you can visibly see what went wrong! // However, drawing the error once will only appear for a single frame // so create an event that keeps it on the screen for some frames Position pos = u->getPosition(); Error lastErr = Broodwar->getLastError(); Broodwar->registerEvent([pos,lastErr](Game*){ Broodwar->drawTextMap(pos, "%c%s", Text::White, lastErr.c_str()); }, // action nullptr, // condition Broodwar->getLatencyFrames()); // frames to run
If your resource depot is idle, but the command to train a new worker has failed, then the code above is triggered. As you can see from the comments, it takes the error that caused the train command to fail and draws it on the screen. Broodwar->drawTextMap(pos, "%c%s", Text::White, lastErr.c_str());
This is the line that does the text drawing. Notice that it's a different method from the one that we used to say "Hello world!" earlier. Broodwar->sendText() sends as if you typed something into chat, Broodwar->drawText() on the other hand, draws the text at a specific location on the map. So basically, if an error occurs, we take the position that that error occurred at, find what type of error it was, and then draw it on the screen at that position. Drawing things on the screen like this obviously doesn't help the AI play the game, but it's very useful for testing purposes so you can see what exactly is going wrong. The more time you spend developing your AI, the more stuff you will find you want to draw on the screen until you start running out of space. // Retrieve the supply provider type in the case that we have run out of supplies UnitType supplyProviderType = u->getType().getRace().getSupplyProvider(); static int lastChecked = 0;
// If we are supply blocked and haven't tried constructing more recently if ( lastErr == Errors::Insufficient_Supply && lastChecked + 400 < Broodwar->getFrameCount() && Broodwar->self()->incompleteUnitCount(supplyProviderType) == 0 ) { lastChecked = Broodwar->getFrameCount();
// Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned); // If a unit was found if ( supplyBuilder ) { if ( supplyProviderType.isBuilding() ) { TilePosition targetBuildLocation = Broodwar->getBuildLocation(supplyProviderType, supplyBuilder->getTilePosition()); if ( targetBuildLocation ) { // Register an event that draws the target build location Broodwar->registerEvent([targetBuildLocation,supplyProviderType](Game*) { Broodwar->drawBoxMap( Position(targetBuildLocation), Position(targetBuildLocation + supplyProviderType.tileSize()), Colors::Blue); }, nullptr, // condition supplyProviderType.buildTime() + 100 ); // frames to run
// Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation ); } } else { // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType ); } } // closure: supplyBuilder is valid } // closure: insufficient supply } // closure: failed to train idle unit
Now we have the section that deals with the construction of additional supply providers. Again, it's written in a way that means that it should work no matter what race the AI is playing. This means that the code is a little bit longer than if it only worked for one race, because obviously different races have different ways of providing supply. UnitType supplyProviderType = u->getType().getRace().getSupplyProvider(); Here we check the type of supply provider that we need. This is done by checking which race we are and then checking the type of supply provider that that races uses; this value is then stored in the variable supplyProviderType. So take Terran for example. We will take u, which is a pointer to one of our units, then check what type of unit (getType()) u is. Once we know the type, we check which race that type of unit belongs to (getRace()). Once we know the race, we can check what type of supply provider that race uses (getSupplyProvider()). Now we know the type of unit (UnitType) which we need to create, We then store this in the variable supplyProviderType so that we can use it later. // If we are supply blocked and haven't tried constructing more recently if ( lastErr == Errors::Insufficient_Supply && lastChecked + 400 < Broodwar->getFrameCount() && Broodwar->self()->incompleteUnitCount(supplyProviderType) == 0 ) { lastChecked = Broodwar->getFrameCount();
This code checks the type of error that we registered earlier. If it was an insufficient supply error then we are going to want to create more supply providers. However, remember that this method is called every frame. If we don't add in any additional checks, the AI would just spam as many supply providers as it could afford. This is because it would check "am i supply blocked?: yes" and queue one up, then 1 frame later it would again check "am i supply blocked?: still yes" and add an additional supply provider. It would keep doing this every frame until a supply provider was completed. So if we are a supply blocked zerg, on the first supply blocked frame, we will construct an overlord. Then before that overlord is even at 1%, we will construct another one, and we will keep doing that every frame that we have the money and larvae to do so until the first overlord is completed. Obviously we don't want that many overlords, so we need to prevent this from happening. ExampleAIModule handles this by recording the number of the frame that we last checked for supply block. If the time we last checked was less than 400 frames ago (roughly 17 seconds) then we will ignore the fact that we are still supply blocked, because we should already have something in production. // Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned); // If a unit was found if ( supplyBuilder ) { if ( supplyProviderType.isBuilding() ) { TilePosition targetBuildLocation = Broodwar->getBuildLocation(supplyProviderType, supplyBuilder->getTilePosition()); if ( targetBuildLocation ) { // Register an event that draws the target build location Broodwar->registerEvent([targetBuildLocation,supplyProviderType](Game*) { Broodwar->drawBoxMap( Position(targetBuildLocation), Position(targetBuildLocation + supplyProviderType.tileSize()), Colors::Blue); }, nullptr, // condition supplyProviderType.buildTime() + 100 ); // frames to run
// Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation ); } } else { // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType ); } } // closure: supplyBuilder is valid } // closure: insufficient supply
Finally, we have the code that deals with the actual creation of new supply providers. This is done by attempting to find a unit which can construct supply providers. If we find one then we can start constructing a supply provider. If no such unit exists, then we must be playing zerg, so we can train a new supply provider from our hatchery instead. // Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned);
When looking for someone to build our supply depot/pylon, we want to make sure that we are getting the right type of unit. Earlier, we stored the type of supply provider we need in supplyProviderType, now we are going to use it to check the type of unit that can construct it. This is done using supplyProviderType.whatBuilds().first. the whatBuilds() method returns a pair, where the first value is the type of unit and the second value is the number of those units required. In StarCraft you can only have 1 worker constructing a building, so the second value is usually 1; the only real exception to this is Archons I guess, which will return <Protoss_High_Templar, 2>. Now that we know that we are looking for an SCV or Probe, we need to check that we aren't selecting one that is already doing something important. We do this by making sure that the worker we select is either idle or mining minerals: we don't want to use our scouting worker or a worker already constructing a building to build the new supply provider. Ok so we have a builder, and we know what we want to build; now we just need a location. ExampleAIModule uses getBuildLocation(). This one is actually new to me; I'm used to BWAPI 3.7.4 so maybe it's a new feature, or maybe I just wasn't aware of it before. But when I started making an AI I spent ages messing around trying to make a system for finding suitable build locations; but it looks like now you can just call this method, as long as you don't care too much about the exact location of the building. The documentation says getBuildLocation():Retrieves a basic build position just as the default Computer AI would. which doesn't sound too bad. My AI's method of finding building positions sometimes leads to it occidentally walling itself in. I might try testing this one instead of mine and see which is more effective. Anyway, now we have a build position too so we can construct that pesky supply provider. // Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation );
This is the bit that does the actual build command. supplyBuilder is our worker, and we are ordering him to build. The build command requires 2 values, the type of building and the location that we want to build it at. We stored the building type in supplyProviderType so we can use that, and we stored the result of getBuildLocation() in the variable targetBuildLocation, so we can use that. If we're zerg instead, then this is the line that does the overlord production: // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType );
In this case, supplyBuilder is our Hatchery rather than an SCV or Probe and we are going to train a unit rather than building a structure. The train command only requires one value, which is the type of unit to be trained, so we can give it supplyProviderType, which will be Zerg_Overlord. So hopefully now you have an idea of how the ExampleAIModule goes about automatically mining, training workers and creating additional supply providers. If we want an AI that does anything else, we're going to need to do it ourselves.
Modifying the ExampleAIModule + Show Spoiler +An AI that just makes SCVs and supply depots isn't very interesting, so let's make it do something cooler! Let's make the AI perform a simple strategy; a 4 pool. 1. Building a spawning Pool So firstly, we need to make the AI build a spawning pool. We already have a loop that iterates through all our units, so we can use that to find a worker and construct the pool. //find a location for spawning pool and construct it TilePosition buildPosition = Broodwar->getBuildLocation(UnitTypes::Zerg_Spawning_Pool, u->getTilePosition()); u->build(UnitTypes::Zerg_Spawning_Pool, buildPosition);
In order to build the pool, we need to find a suitable location to build it. To do this, we can use Broodwar->getBuildLocation() which we learned about earlier. We want to build a Spawning pool so lets put that unit type into the first argument, and we want to build it near to where the drone currently is, so lets put the current tile position of the drone in the second argument (u->getTilePosition()). TilePositions are one of the 3 types of position that are used by BWAPI. The other two are Postion and WalkPosition. All 3 types are stored as a grid location which corresponds to a point on the map. Position is the smallest size and corresponds to a single pixel; WalkPosition is a square of size 8x8 pixels and TilePosition is a square of size 32x32 pixels. When units move around the map, they move from one WalkPosition to another. Building placement however is measured in terms of TilePositions. Position(0,0) corresponds to the pixel in the very to left corner of the map. Position (1,2) is shifted one pixel to the right and 2 pixels down. Even though our drone moves around in terms of WalkPositions, we can still find out which TilePosition it is currently occupying; this is done using u->getTilePosition() (remember u here is a pointer to our drone). Now that we have a location for the spawning pool, we can order our drone to construct it. This is done using u->build(). We now have code that finds a drone and tells it to make a spawning pool. However, there's several problems with this. The main problem is that, since this code is in onFrame(), it is going to be executed every frame. This means that every frame, we are going to tell every one of it's drones to build a spawning pool. We only want one spawning pool though, so we need a way to stop it from building more than one. We also don't check whether we have enough money to actually build the pool; so we are constantly spamming build commands even when we have no money. Making sure that we have enough minerals is fairly straight forward: if (Broodwar->self()->minerals() >= UnitTypes::Zerg_Spawning_Pool.mineralPrice()) { //build pool }
Before we attempt to build the pool, we should check how many minerals we have. This can be done using Broodwar->self()->minerals(). This returns the amount of minerals we currently have. We then want to compare this number against the cost of a spawning pool. You could just check if the minerals are >= 200, but it's usually better to avoid putting numbers in your code like this. We can check unit stats and costs by looking up their UnitType. We can find the cost of a UnitType by using UnitType.mineralPrice(). Next we need to make sure we only build 1 spawning pool. This means we only want to build a spawning pool if we have not already started building one. One way of doing this is by having a bool variable which can store the current spawning pool status. We then just need to check if we have started building a pool, and if we have we can change the bool to true. If the bool value is false then we haven't yet built a pool and we can try to build one, but if it's true then we already have one so we can skip trying to build one. I created a class variable called pool in ExampleAIModule.h. Next, in onStart(), we can initialise pool: pool = false. Then back in onFrame(), we can add another condition before attempting to build a pool: if (!pool && (Broodwar->self()->minerals() >= UnitTypes::Zerg_Spawning_Pool.mineralPrice())) { //build pool }
So now, as well as checking that we have enough money, we check that we don't already have a pool (if(!pool ...). Now we just need to check if we've started a pool. if ((!pool) && (Broodwar->self()->minerals() >= UnitTypes::Zerg_Spawning_Pool.mineralPrice())) { //find a location for spawning pool and construct it TilePosition buildPosition = Broodwar->getBuildLocation(BWAPI::UnitTypes::Zerg_Spawning_Pool, u->getTilePosition()); u->build(UnitTypes::Zerg_Spawning_Pool, buildPosition); pool = true; }
So now this is what the final code looks like. If we have enough money and haven't issued a build pool command before then we will tell a drone to build a pool. After that we set the value of pool to true so that we don't send any more unnecessary commands or tell multiple workers to do the same thing. 2. Making lings We already have a line which builds workers whenever we have idle larvae, but we don't want to build any workers because we're 4 pooling, so lets just change it to build zerglings instead. Both are produced from larvae at the hatchery so we don't really need to make any other changes. I also added in a condition to make sure that pool is true before we start attempting to build lings, just so it spams less commands. if (pool && (u->isIdle() && !u->train(UnitTypes::Zerg_Zergling) ))
Easy huh. 3. Attacking! Now our AI can successfully build a pool and start making lings, but we still need to attack with those lings. I'm afraid I'm going to wuss out a bit at this point and just enable the CompleteMapInformation flag. Turning on this flag gives the AI complete information about it's opponents units even through fog of war. Normally I would turn this flag off because it's against the rules for the AIIDE (and other) tournament. It's also less cool to have a bot with a maphack than one that knows how to scout. However, it seems like the library I used to use for map analysis ( BWTA) no longer works with the latest version of BWAPI; and I think coming up with our own solution to the problem of scouting is a bit beyond the scope of this guide so to simply things we're just going to leave that element out. If you come up with a way to scout then you can easily just turn the flag back off. Since we don't have to worry about finding our enemy, commanding our lings to attack becomes very simple: if ((u->getType() == UnitTypes::Zerg_Zergling) && u->isIdle()) { Unit closestEnemy = NULL; for (auto &e : Broodwar->enemy()->getUnits()) { if ((closestEnemy == NULL) || (e->getDistance(u) < closestEnemy->getDistance(u))) { closestEnemy = e; } } u->attack(closestEnemy, false); }
edit: Heinermann provided a better solution: if ((u->getType() == UnitTypes::Zerg_Zergling) && u->isIdle()) { u->attack(u->getClosestUnit(Filters::IsEnemy)); }
We can add this code into the loop in onFrame(). When looping through all of our units, if we find an idle zergling then we can issue an attack command. We still need to find a target though so I made a simple way of finding a target by finding the closest enemy. To do this we iterate through all of the enemy units and compare their distances from our zergling. We store the unit with the smallest distance in the closestEnemy variable. When we've checked all of the enemy units, we can then issue an attack command on whichever was closest. Our AI now builds a pool, trains lings and tells them to attack. Final Code: (Dll.cpp remains unchanged) ExampleAIModule.cpp + Show Spoiler + #include "ExampleAIModule.h" #include <iostream>
using namespace BWAPI; using namespace Filter;
void ExampleAIModule::onStart() { // Hello World! Broodwar->sendText("Hello world!");
// Print the map name. // BWAPI returns std::string when retrieving a string, don't forget to add .c_str() when printing! Broodwar << "The map is " << Broodwar->mapName() << "!" << std::endl;
// Enable the UserInput flag, which allows us to control the bot and type messages. Broodwar->enableFlag(Flag::UserInput);
// Uncomment the following line and the bot will know about everything through the fog of war (cheat). Broodwar->enableFlag(Flag::CompleteMapInformation);
// Set the command optimization level so that common commands can be grouped // and reduce the bot's APM (Actions Per Minute). Broodwar->setCommandOptimizationLevel(2);
// Check if this is a replay if ( Broodwar->isReplay() ) {
// Announce the players in the replay Broodwar << "The following players are in this replay:" << std::endl; // Iterate all the players in the game using a std:: iterator Playerset players = Broodwar->getPlayers(); for(auto p : players) { // Only print the player if they are not an observer if ( !p->isObserver() ) Broodwar << p->getName() << ", playing as " << p->getRace() << std::endl; }
} else // if this is not a replay { // Retrieve you and your enemy's races. enemy() will just return the first enemy. // If you wish to deal with multiple enemies then you must use enemies(). if ( Broodwar->enemy() ) // First make sure there is an enemy Broodwar << "The matchup is " << Broodwar->self()->getRace() << " vs " << Broodwar->enemy()->getRace() << std::endl; }
pool = false; }
void ExampleAIModule::onEnd(bool isWinner) { // Called when the game ends if ( isWinner ) { // Log your win here! } }
void ExampleAIModule::onFrame() { // Called once every game frame
// Display the game frame rate as text in the upper left area of the screen Broodwar->drawTextScreen(200, 0, "FPS: %d", Broodwar->getFPS() ); Broodwar->drawTextScreen(200, 20, "Average FPS: %f", Broodwar->getAverageFPS() );
// Return if the game is a replay or is paused if ( Broodwar->isReplay() || Broodwar->isPaused() || !Broodwar->self() ) return;
// Prevent spamming by only running our onFrame once every number of latency frames. // Latency frames are the number of frames before commands are processed. if ( Broodwar->getFrameCount() % Broodwar->getLatencyFrames() != 0 ) return;
// Iterate through all the units that we own for (auto &u : Broodwar->self()->getUnits()) { // Ignore the unit if it no longer exists // Make sure to include this block when handling any Unit pointer! if ( !u->exists() ) continue;
// Ignore the unit if it has one of the following status ailments if ( u->isLockedDown() || u->isMaelstrommed() || u->isStasised() ) continue;
// Ignore the unit if it is in one of the following states if ( u->isLoaded() || !u->isPowered() || u->isStuck() ) continue;
// Ignore the unit if it is incomplete or busy constructing if (!u->isCompleted() || u->isConstructing() ) continue;
// Finally make the unit do some stuff!
if ((u->getType() == UnitTypes::Zerg_Zergling) && u->isIdle()) { Unit closestEnemy = NULL; for (auto &e : Broodwar->enemy()->getUnits()) { if ((closestEnemy == NULL) || (e->getDistance(u) < closestEnemy->getDistance(u))) { closestEnemy = e; } } u->attack(closestEnemy, false); }
// If the unit is a worker unit if ( u->getType().isWorker() ) { if ((!pool) && (Broodwar->self()->minerals() >= UnitTypes::Zerg_Spawning_Pool.mineralPrice())) { //find a location for spawning pool and construct it TilePosition buildPosition = Broodwar->getBuildLocation(BWAPI::UnitTypes::Zerg_Spawning_Pool, u->getTilePosition()); u->build(UnitTypes::Zerg_Spawning_Pool, buildPosition); pool = true; }
// if our worker is idle if ( u->isIdle() ) { // Order workers carrying a resource to return them to the center, // otherwise find a mineral patch to harvest. if ( u->isCarryingGas() || u->isCarryingMinerals() ) { u->returnCargo(); } else if ( !u->getPowerUp() ) // The worker cannot harvest anything if it { // is carrying a powerup such as a flag // Harvest from the nearest mineral patch or gas refinery if ( !u->gather( u->getClosestUnit( IsMineralField || IsRefinery )) ) { // If the call fails, then print the last error message Broodwar << Broodwar->getLastError() << std::endl; }
} // closure: has no powerup } // closure: if idle
} else if ( u->getType().isResourceDepot() ) // A resource depot is a Command Center, Nexus, or Hatchery {
// Order the depot to construct more workers! But only when it is idle. if (pool && (u->isIdle() && !u->train(UnitTypes::Zerg_Zergling) )) { // If that fails, draw the error at the location so that you can visibly see what went wrong! // However, drawing the error once will only appear for a single frame // so create an event that keeps it on the screen for some frames Position pos = u->getPosition(); Error lastErr = Broodwar->getLastError(); Broodwar->registerEvent([pos,lastErr](Game*){ Broodwar->drawTextMap(pos, "%c%s", Text::White, lastErr.c_str()); }, // action nullptr, // condition Broodwar->getLatencyFrames()); // frames to run
// Retrieve the supply provider type in the case that we have run out of supplies UnitType supplyProviderType = u->getType().getRace().getSupplyProvider(); static int lastChecked = 0;
// If we are supply blocked and haven't tried constructing more recently if ( lastErr == Errors::Insufficient_Supply && lastChecked + 400 < Broodwar->getFrameCount() && Broodwar->self()->incompleteUnitCount(supplyProviderType) == 0 ) { lastChecked = Broodwar->getFrameCount();
// Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned); // If a unit was found if ( supplyBuilder ) { if ( supplyProviderType.isBuilding() ) { TilePosition targetBuildLocation = Broodwar->getBuildLocation(supplyProviderType, supplyBuilder->getTilePosition()); if ( targetBuildLocation ) { // Register an event that draws the target build location Broodwar->registerEvent([targetBuildLocation,supplyProviderType](Game*) { Broodwar->drawBoxMap( Position(targetBuildLocation), Position(targetBuildLocation + supplyProviderType.tileSize()), Colors::Blue); }, nullptr, // condition supplyProviderType.buildTime() + 100 ); // frames to run
// Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation ); } } else { // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType ); } } // closure: supplyBuilder is valid } // closure: insufficient supply } // closure: failed to train idle unit
}
} // closure: unit iterator }
void ExampleAIModule::onSendText(std::string text) {
// Send the text to the game if it is not being processed. Broodwar->sendText("%s", text.c_str());
// Make sure to use %s and pass the text as a parameter, // otherwise you may run into problems when you use the %(percent) character!
}
void ExampleAIModule::onReceiveText(BWAPI::Player player, std::string text) { // Parse the received text Broodwar << player->getName() << " said \"" << text << "\"" << std::endl; }
void ExampleAIModule::onPlayerLeft(BWAPI::Player player) { // Interact verbally with the other players in the game by // announcing that the other player has left. Broodwar->sendText("Goodbye %s!", player->getName().c_str()); }
void ExampleAIModule::onNukeDetect(BWAPI::Position target) {
// Check if the target is a valid position if ( target ) { // if so, print the location of the nuclear strike target Broodwar << "Nuclear Launch Detected at " << target << std::endl; } else { // Otherwise, ask other players where the nuke is! Broodwar->sendText("Where's the nuke?"); }
// You can also retrieve all the nuclear missile targets using Broodwar->getNukeDots()! }
void ExampleAIModule::onUnitDiscover(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitEvade(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitShow(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitHide(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitCreate(BWAPI::Unit unit) { if ( Broodwar->isReplay() ) { // if we are in a replay, then we will print out the build order of the structures if ( unit->getType().isBuilding() && !unit->getPlayer()->isNeutral() ) { int seconds = Broodwar->getFrameCount()/24; int minutes = seconds/60; seconds %= 60; Broodwar->sendText("%.2d:%.2d: %s creates a %s", minutes, seconds, unit->getPlayer()->getName().c_str(), unit->getType().c_str()); } } }
void ExampleAIModule::onUnitDestroy(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitMorph(BWAPI::Unit unit) { if ( Broodwar->isReplay() ) { // if we are in a replay, then we will print out the build order of the structures if ( unit->getType().isBuilding() && !unit->getPlayer()->isNeutral() ) { int seconds = Broodwar->getFrameCount()/24; int minutes = seconds/60; seconds %= 60; Broodwar->sendText("%.2d:%.2d: %s morphs a %s", minutes, seconds, unit->getPlayer()->getName().c_str(), unit->getType().c_str()); } } }
void ExampleAIModule::onUnitRenegade(BWAPI::Unit unit) { }
void ExampleAIModule::onSaveGame(std::string gameName) { Broodwar << "The game was saved to \"" << gameName << "\"" << std::endl; }
void ExampleAIModule::onUnitComplete(BWAPI::Unit unit) { }
ExampleAIModule.h + Show Spoiler + #pragma once #include <BWAPI.h>
// Remember not to use "Broodwar" in any global class constructor!
class ExampleAIModule : public BWAPI::AIModule { bool pool; public: // Virtual functions for callbacks, leave these as they are. virtual void onStart(); virtual void onEnd(bool isWinner); virtual void onFrame(); virtual void onSendText(std::string text); virtual void onReceiveText(BWAPI::Player player, std::string text); virtual void onPlayerLeft(BWAPI::Player player); virtual void onNukeDetect(BWAPI::Position target); virtual void onUnitDiscover(BWAPI::Unit unit); virtual void onUnitEvade(BWAPI::Unit unit); virtual void onUnitShow(BWAPI::Unit unit); virtual void onUnitHide(BWAPI::Unit unit); virtual void onUnitCreate(BWAPI::Unit unit); virtual void onUnitDestroy(BWAPI::Unit unit); virtual void onUnitMorph(BWAPI::Unit unit); virtual void onUnitRenegade(BWAPI::Unit unit); virtual void onSaveGame(std::string gameName); virtual void onUnitComplete(BWAPI::Unit unit); // Everything below this line is safe to modify.
};
And finally here's a link to the VS project with all the changes already made to it: http://www.mediafire.com/download/8e78e2q268easny/ExampleAIModule4pool.vcxproj
Testing + Show Spoiler +Now that we've updated the ExampleAIModule to perform a sick 4 pool, we need to test that it works ok. We can do this by following the same steps we originally followed to compile and run the ExampleAIModule the first time. Compile, or 'build', the solution in Visual Studio, then copy the compiled dll file from the BWAPI/Releases/ folder into StarCraft/bwapi-date/AI. Then run StarCraft through ChaosLauncher. You should hopefully see the AI build a spawning pool, make some lings and attack. Here's a link to the final compiled AI (called 4pooler.dll): http://www.mediafire.com/download/2ak4edihtbfid06/4pooler.dllHere's a link to a replay of the SCBW default AI getting rekt by 4pooler: http://www.mediafire.com/download/8mealsa0a3xp0fz/4pooler.rep
Further Development + Show Spoiler +Obviously, this is a very simple AI which would be really easy to beat for a human player. Some obvious weaknesses are: - It can't transition out of mining with 3 drones and attacking with lings. If it's opponent manages to defend the initial lings, it will just keep going and will never stop until one player or the other is dead. - It can't micro at all. The zerglings just attack the closest enemy and don't pay any attention to more important targets or retreat when they are going to die or anything like that. - It can't recover from losing a structure. If the AI's opponent came and killed the drone before it started building the pool, or if the pool died, then it wouldn't attempt to build another one because the pool variable would still be set to true. In this scenario, the AI would just stop doing anything, because it would be unable to train more zerglings. - It has CompleteMapInformation turned on. Ideally we need a solution to scouting our opponent so that the AI doesn't need to cheat. Even though our AI is only performing a simple strategy, there's still loads of ways that we could improve it in order to be more effective. If you check out some of the more successful AI systems (you can find bots and replays here) you will see that they are much more sophisticated than the crude 4 pool AI we just created. Some of them can scout and micro and are much more flexible than our AI. By flexible I mean that they are capable of adapting to their opponent or to unexpected situations.
If you would rather start developing from a full functional AI rather than the default one then UAlbertaBot's github page can be found here. Also, here's my AI from last year. It's a bit of a mess though. Look at UAlbertaBot instead if you want a good example of a functioning AI system. Thanks to David Churchill for creating UAlbertaBot and making it available; I studied it and stole/copied sections of it for my own system so I probably wouldn't have got very far without it.
Some useful links: - AIIDE StarCraft Competition: http://webdocs.cs.ualberta.ca/~cdavid/starcraftaicomp/ - CIG StarCraft Competition: http://cilab.sejong.ac.kr/sc_competition/ - SCAII StarCraft Tournament: http://www.sscaitournament.com/ - A Survey of Real-Time Strategy Game AI Research and Competition in StarCraft: http://webdocs.cs.ualberta.ca/~cdavid/pdf/starcraft_survey.pdf - BWAPI releases page: https://github.com/bwapi/bwapi/releases - BWAPI Documentation: http://bwapi.github.io/index.html
Hopefully this will be of interest to someone. I'm not really sure who this is aimed at or if the guide is too basic or too difficult. If you have any questions then I'm happy to answer them. If you just want to tell me I'm wrong and I suck then that's ok too I guess.
|
Always wanted to try and make an AI using all the stuff people have developed for BW. Thanks for the guide! This may get me started ^^
|
I am definitely going to try to follow your guide step by step. As far as guides go this one is quality. Thank you for the obvious time and effort you have put into this.
|
Bookmarked!
Going to come back and use this when I have the time to pursue this as a hobby
Thanks for writing this out!
|
Croatia9446 Posts
This is a great post; thank you for writing it.
Spotlighted!
|
Hmmm... hope you guys run this again next year, I'll join. I'm just starting to learn C and am proficient enough in Java.
|
Why do people only make custom AI for bw and not for sc2?
|
On May 16 2015 00:49 mooose wrote:+ Show Spoiler +The signups for the 2015 AIIDE StarCraft AI Competition were announced this week, which has been good motivation for me to get back into working on my AI. I competed last year and my AI came 9th out of 18 competitors; with a win rate of just over 50%. I was fairly happy with that as result for my first attempt, but I'm hoping to do better this year. I was previously working on my AI mostly on my desktop at home, but now I'm in Japan I only have this crappy laptop so I've had to install a bunch of stuff to be able to continue. I thought that since I've been having to set everything up again, I might try writing a guide for any one else who is interested in getting into SC:BW AI programming. I think it's worth noting that I'm not a professional programmer, and I have fairly limited formal education (1 year). If you look at the code for my AI, a lot of it is a horrible mess, this is partly because I was learning C++ while working on it, and partly because I'm still not that great at it. Despite that, I think I'm qualified to at least explain the basics of how to start working on a BW AI system. The AI project last year was for my MSc dissertation, and I got a pretty good grade, so it can't have been completely horrible lol. Anyway, I apologise in advance if I give out any incorrect information or if any of my example code is ugly as fuck. Also, all credit for the ExampleAIModule code that we'll be looking at in this guide goes to heinermann, the guy behind BWAPI. Background: why develop AI for SC:BW? + Show Spoiler +The best AI systems for games like Chess are really good, and can compete with the best players in the world. The best AI systems for StarCraft are complete garbage in comparison, and can be defeated by even mediocre players. This is because StarCraft is a game with a much higher level of complexity. In Chess, there are a huge number of possible moves that can be made, but in StarCraft the number is much higher. In comparison to a StarCraft map, a Chess board is fairly small with a limited set of pieces and locations that pieces can occupy. A StarCraft map can have hundreds of units, and each of these units can occupy thousands of different locations on the map, and also have multiple abilities and statuses. This is before you even take into account structures, resource gathering or unit production. Another factor which makes StarCraft AI programming difficult are the real-time constraints. To use a Chess AI as an example again; in Chess, players take turns, so the AI has all the time it needs to calculate possible moves and weigh up which is most effective before ending its turn. In contrast, StarCraft is played in real-time, which means that the AI doesn't have time to wait and calculate all the possible moves. Things like tree searches would take ages in a StarCraft game because of the previously mentioned enormous complexity, and the fact that the game is played in real-time means that that available calculation is quite small (in the AIIDE competition, an AI which regularly takes longer than 55ms to finish a frame will be disqualified from that game). So basically SC:BW creates an interesting set of challenges for AI programmers. It's also a great game, and testing mostly involves watching your AI play, which is quite fun. If you want a more detailed, and better written account of the challenges and previously explored solutions in StarCraft AI programming then I would recommend this paper. It's a pretty good introduction because it talks about the various challenges facing SC AI programmers and talks about some of the more successful AI systems and their architectures. Before getting started: required skills + Show Spoiler + While you don't need to go in as some kind of programming wizard or AI expert, I would recommend that you have some kind of programming experience or training before getting involved with a project like this. Before I started working on my AI, I had never touched C++ or Visual Studio before and had no idea about anything to do with AI, so it's definitely possible to learn as you go. However I was studying Computer Science at University at the time, and had already taken courses in C and Object Oriented Programming so picking up C++ wasn't particularly challenging.
If you've never done any programming before, but you think you might be interested, I would recommend following some tutorials or online courses first. Try to familiarise yourself with basic programming concepts and get some experience writing simple programs before you start on this.
However, if you already have some vaguely relevant experience then there's no reason you can't do this. Just have a go; I think it's really fun.
Getting started: installation guide + Show Spoiler +I've only ever done this on Windows 7 and 8. I don't know what works and doesn't work on other operating systems. If you're on Windows 7 or 8 (or possibly others) and you follow these steps then I guess it should work. If not, then maybe write a comment and I may or may not be able to help. All the versions of BWAPI that I have used come with an ExampleAIModule which is a good way to test that you've installed everything correctly. After installing stuff, follow the next steps to compile and run the ExampleAIModule; if it works then you know you're all set up to start coding. Note: If you're using a different version of BWAPI or Visual Studio to me then things might be in slightly different places. Downloading and installing: 1. If you haven't got it already, install SC:BW and update it to version 1.16.1. 2. Download the latest version of the BroodWar Application Programming Interface (BWAPI) (as of writing the latest version is BWAPI 4.1.1 Beta) from here, and install it. You will also need ChaosLauncher, but that should come with BWAPI when you install it. 3. Download the latest version of Visual Studio C++. I downloaded Visual Studio Community 2013 from here but other versions might work too. VS always comes with a million extra things, and you can probably untick some of the boxes if you don't want it all; as long as you get the C++ stuff. Compiling and running the Example AI 4. Go to the directory you installed BWAPI. Inside there should be a folder called ExampleAIModule. Inside this there should be a VC++ Project file called ExampleAIModule.vcxproj; open this in Visual Studio. Note: ExampleAIModule; NOT ExampleAIClient or ExampleTournamentModule. 5. When Visual Studio has finished dicking around and is ready to use, find solution configuration and change it from 'Debug' to 'Release'. It's here. 6. In the Solution Explorer window, right click on ExampleAIModule and click 'Build'. Like this. 7. Check Output window to make sure that the project compiled correctly. It should say "Build: 1 succeeded, 0 failed". If there are any errors then you won't be able to go on to the next steps until they are resolved. It should look something like this. 8. Go back to your BWAPI directory (up one level from the ExampleAIModule folder) and find the folder named 'Release'. Inside you should find a file named ExampleAIModule.dll; copy this file. Note: compiling will also create another folder called Release inside the ExampleAIModule folder; this is the wrong folder; you won't find the dll file here. Look inside the Release folder in your BWAPI directory instead. 9. Find where you have StarCraft installed. Inside this directory you hopefully have a folder called bwapi-data, inside this is another folder called AI. Go to Starcraft/bwapi-data/AI/ and paste the newly created ExampleAIModule.dll inside. Note: I suppose if these folders aren't there, you can probably create them. 10. Now you need to run ChaosLauncher. Go back to your BWAPI directory. Inside, there should be a folder called ChaosLauncher; inside this should be ChaosLauncher.exe. Make sure to right click and run this as Administrator or you might get some stupid error message. 11. In ChaosLauncher, make sure 'BWAPI Injector (1.16.1) RELEASE' is ticked. 12. Select the 'BWAPI Injector (1.16.1) RELEASE' option and click on the button that says 'Config'; this should open a text file. Near the top of the text file should be a line that says "ai = bwapi-data/AI/ExampleAIModule.dll". If ai is = to something other than "bwapi-data/AI/ExampleAIModule.dll" then the AI won't run; so make sure it's typed in correctly (but it should be there by default). You can now close Config if you want. ChaosLauncher. 13. Click the Start button in ChaosLauncher. This should load SC:BW. Now navigate through the menus to start a single player custom game. Note: this menu screen navigation can be automated from the config file so that clicking Start on ChaosLauncher takes you directly to a game; this isn't necessary though so I haven't put it in the guide. 14. When the game starts, the AI should hopefully immediately start doing it's thing (sometime's it lags a bit at the start, especially the first time). If it says something like 'AI module failed to load' and/or nothing is happening then something has gone wrong. It should look like this. The ExampleAIModule from the version of BWAPI I have downloaded automatically sends it's workers to mine and builds a new worker whenever it's main is idle, but doesn't do much else. If you load into a game and the workers start mining and a new one is being constructed then it's worked. Congratulations, you can now start working on your own AI! Getting started with BWAPI + Show Spoiler +Now that we know that everything has been set up correctly, let's take a look at the ExampleAIModule's code so we can learn how it works. Firstly we should arm ourselves with the BWAPI Documentation. You'll need to use it a lot at first to find the things you're looking for. Now open ExampleAIModule in Visual Studio and have a look. There should be 3 files: Dll.cpp, ExampleAIModule.cpp and ExampleAIModule.h. I'll be honest, I don't understand the code in Dll.cpp, and I've never modified it so let's just leave that one alone; it's probably something to do with turning your code into a dll file. Lets have a look at the header file, ExampleAIModule.h: ExampleAIModule.h code + Show Spoiler + #pragma once #include <BWAPI.h>
// Remember not to use "Broodwar" in any global class constructor!
class ExampleAIModule : public BWAPI::AIModule { public: // Virtual functions for callbacks, leave these as they are. virtual void onStart(); virtual void onEnd(bool isWinner); virtual void onFrame(); virtual void onSendText(std::string text); virtual void onReceiveText(BWAPI:layer player, std::string text); virtual void onPlayerLeft(BWAPI:layer player); virtual void onNukeDetect(BWAPI:osition target); virtual void onUnitDiscover(BWAPI::Unit unit); virtual void onUnitEvade(BWAPI::Unit unit); virtual void onUnitShow(BWAPI::Unit unit); virtual void onUnitHide(BWAPI::Unit unit); virtual void onUnitCreate(BWAPI::Unit unit); virtual void onUnitDestroy(BWAPI::Unit unit); virtual void onUnitMorph(BWAPI::Unit unit); virtual void onUnitRenegade(BWAPI::Unit unit); virtual void onSaveGame(std::string gameName); virtual void onUnitComplete(BWAPI::Unit unit); // Everything below this line is safe to modify.
};
As you can see, there's a class with a bunch of methods called onSomething(). These are called whenever the in-game event or condition that they represent occurs. For example, onNukeDetect() is called when a Nuke is detected. They all have pretty self-explanatory names to be honest so I won't go into detail explaining them all. If you are confused about one, then you can always check the BWAPI documentation. The ExampleAIModule doesn't do much with most of these methods; the main ones that it uses are onStart() and onFrame(). As you might have guessed, onStart() is called at the start of the game. onFrame() is called once per in-game frame (24 times per second in a game on the fastest setting). As was noted in the installation instructions, the ExampleAIModule simply builds workers and sends idle workers to mine. Let's take a look in ExampleAIModule.cpp to see how it does that. ExampleAIModule.onStart() code + Show Spoiler + void ExampleAIModule::onStart() { // Hello World! Broodwar->sendText("Hello world!");
// Print the map name. // BWAPI returns std::string when retrieving a string, don't forget to add .c_str() when printing! Broodwar << "The map is " << Broodwar->mapName() << "!" << std::endl;
// Enable the UserInput flag, which allows us to control the bot and type messages. Broodwar->enableFlag(Flag::UserInput);
// Uncomment the following line and the bot will know about everything through the fog of war (cheat). //Broodwar->enableFlag(Flag::CompleteMapInformation);
// Set the command optimization level so that common commands can be grouped // and reduce the bot's APM (Actions Per Minute). Broodwar->setCommandOptimizationLevel(2);
// Check if this is a replay if ( Broodwar->isReplay() ) {
// Announce the players in the replay Broodwar << "The following players are in this replay:" << std::endl; // Iterate all the players in the game using a std:: iterator Playerset players = Broodwar->getPlayers(); for(auto p : players) { // Only print the player if they are not an observer if ( !p->isObserver() ) Broodwar << p->getName() << ", playing as " << p->getRace() << std::endl; }
} else // if this is not a replay { // Retrieve you and your enemy's races. enemy() will just return the first enemy. // If you wish to deal with multiple enemies then you must use enemies(). if ( Broodwar->enemy() ) // First make sure there is an enemy Broodwar << "The matchup is " << Broodwar->self()->getRace() << " vs " << Broodwar->enemy()->getRace() << std::endl; }
}
Ok so there's a bunch of stuff here in the onStart() method, with some nice comments so I don't really need to explain it. At the start there is this line: Broodwar->sendText("Hello world!"); This just prints "Hello world!" in the in-game chat. You can use Broodwar->sendText() to write whatever you want. This is not much use unless you're planning on incorporating a "from?" rush into your AI's strategy. There's also a few flags set here, for example: //Broodwar->enableFlag(Flag::CompleteMapInformation); This one is commented out; this means that the AI does not have complete map information. If you enable it then the AI will basically get a maphack. If you want to compete in any of the AI tournaments then keep this flag turned off. The other flag is UserInput; this is turned on; it just means that you are able to perform actions even when the AI is playing. This can be useful if your AI gets stuck doing something while you are trying to test something specific; you can take control and get it to the point that you want it to be at. But obviously if you are going to enter your AI in an AI tournament, you aren't allowed to play too. Most of the rest of this method is just printing information about the game. It's a pretty useful one though, for doing all the stuff that you want done once at the start of the game. Next we'll look at the most important method; onFrame(). It's kinda big so pasting the whole thing probably isn't much help but here it is anyway for reference. ExampleAIModule.onFrame() code + Show Spoiler + void ExampleAIModule::onFrame() { // Called once every game frame
// Display the game frame rate as text in the upper left area of the screen Broodwar->drawTextScreen(200, 0, "FPS: %d", Broodwar->getFPS() ); Broodwar->drawTextScreen(200, 20, "Average FPS: %f", Broodwar->getAverageFPS() );
// Return if the game is a replay or is paused if ( Broodwar->isReplay() || Broodwar->isPaused() || !Broodwar->self() ) return;
// Prevent spamming by only running our onFrame once every number of latency frames. // Latency frames are the number of frames before commands are processed. if ( Broodwar->getFrameCount() % Broodwar->getLatencyFrames() != 0 ) return;
// Iterate through all the units that we own for (auto &u : Broodwar->self()->getUnits()) { // Ignore the unit if it no longer exists // Make sure to include this block when handling any Unit pointer! if ( !u->exists() ) continue;
// Ignore the unit if it has one of the following status ailments if ( u->isLockedDown() || u->isMaelstrommed() || u->isStasised() ) continue;
// Ignore the unit if it is in one of the following states if ( u->isLoaded() || !u->isPowered() || u->isStuck() ) continue;
// Ignore the unit if it is incomplete or busy constructing if ( !u->isCompleted() || u->isConstructing() ) continue;
// Finally make the unit do some stuff!
// If the unit is a worker unit if ( u->getType().isWorker() ) { // if our worker is idle if ( u->isIdle() ) { // Order workers carrying a resource to return them to the center, // otherwise find a mineral patch to harvest. if ( u->isCarryingGas() || u->isCarryingMinerals() ) { u->returnCargo(); } else if ( !u->getPowerUp() ) // The worker cannot harvest anything if it { // is carrying a powerup such as a flag // Harvest from the nearest mineral patch or gas refinery if ( !u->gather( u->getClosestUnit( IsMineralField || IsRefinery )) ) { // If the call fails, then print the last error message Broodwar << Broodwar->getLastError() << std::endl; }
} // closure: has no powerup } // closure: if idle
} else if ( u->getType().isResourceDepot() ) // A resource depot is a Command Center, Nexus, or Hatchery {
// Order the depot to construct more workers! But only when it is idle. if ( u->isIdle() && !u->train(u->getType().getRace().getWorker()) ) { // If that fails, draw the error at the location so that you can visibly see what went wrong! // However, drawing the error once will only appear for a single frame // so create an event that keeps it on the screen for some frames Position pos = u->getPosition(); Error lastErr = Broodwar->getLastError(); Broodwar->registerEvent([pos,lastErr](Game*){ Broodwar->drawTextMap(pos, "%c%s", Text::White, lastErr.c_str()); }, // action nullptr, // condition Broodwar->getLatencyFrames()); // frames to run
// Retrieve the supply provider type in the case that we have run out of supplies UnitType supplyProviderType = u->getType().getRace().getSupplyProvider(); static int lastChecked = 0;
// If we are supply blocked and haven't tried constructing more recently if ( lastErr == Errors::Insufficient_Supply && lastChecked + 400 < Broodwar->getFrameCount() && Broodwar->self()->incompleteUnitCount(supplyProviderType) == 0 ) { lastChecked = Broodwar->getFrameCount();
// Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned); // If a unit was found if ( supplyBuilder ) { if ( supplyProviderType.isBuilding() ) { TilePosition targetBuildLocation = Broodwar->getBuildLocation(supplyProviderType, supplyBuilder->getTilePosition()); if ( targetBuildLocation ) { // Register an event that draws the target build location Broodwar->registerEvent([targetBuildLocation,supplyProviderType](Game*) { Broodwar->drawBoxMap( Position(targetBuildLocation), Position(targetBuildLocation + supplyProviderType.tileSize()), Colors::Blue); }, nullptr, // condition supplyProviderType.buildTime() + 100 ); // frames to run
// Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation ); } } else { // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType ); } } // closure: supplyBuilder is valid } // closure: insufficient supply } // closure: failed to train idle unit
}
} // closure: unit iterator }
As I mentioned before, onFrame() is called every frame(). The majority of the code in this method is contained within this loop: // Iterate through all the units that we own for (auto &u : Broodwar->self()->getUnits()) { //stuff }
So this means that each frame, the AI will go through each of the units (by units we mean all the things under the player's control, including structures) that it owns one at a time and do something. The first few lines inside this for loop are just checks to make sure that the unit we are trying to manipulate actually exists and isn't in some kind of status that makes it unusable. After that, we come to this section: // If the unit is a worker unit if ( u->getType().isWorker() ) { // if our worker is idle if ( u->isIdle() ) { // Order workers carrying a resource to return them to the center, // otherwise find a mineral patch to harvest. if ( u->isCarryingGas() || u->isCarryingMinerals() ) { u->returnCargo(); } else if ( !u->getPowerUp() ) // The worker cannot harvest anything if it { // is carrying a powerup such as a flag // Harvest from the nearest mineral patch or gas refinery if ( !u->gather( u->getClosestUnit( IsMineralField || IsRefinery )) ) { // If the call fails, then print the last error message Broodwar << Broodwar->getLastError() << std::endl; }
} // closure: has no powerup } // closure: if idle
}
This is the section that makes the workers automatically gather resources. It goes through each unit and, if it is a worker, checks whether it is idle. If it finds an idle worker, and that worker is carrying minerals or gas, then it tells them to return those resources to a resource depot (CC/Nexus/Hatchery). If the idle worker isn't carrying anything then the nearest resource patch is found and the worker is commanded to gather from it. Remember this is called every frame, which means that the AI should never have an idle worker for more than 1 frame, or roughly 1/24th of a second. Next, we have the code that automatically builds additional workers: else if ( u->getType().isResourceDepot() ) // A resource depot is a Command Center, Nexus, or Hatchery {
// Order the depot to construct more workers! But only when it is idle. if ( u->isIdle() && !u->train(u->getType().getRace().getWorker()) ) { //do stuff } }
This is still contained within the same for loop as the code that checked for idle workers. But this time we are checking if the unit is a resource depot instead. Resource depot is the general word used for Command Centers, Nexuses, Hatcheries, Lairs and Hives. Notice how all the code here is written using non-race-specific terms like 'worker' and 'resource depot'; this means that it will work equally well for each race. Anyway, as you can see, if we find a resource depot, then we check if it is idle (not training a unit or researching a tech). If it is idle, then we tell it to train a worker. u->train(u->getType().getRace().getWorker())
This is the part that trains the worker. However, it is contained within a condition so that if it fails, an error can be drawn so that we can check what the problem was. // If that fails, draw the error at the location so that you can visibly see what went wrong! // However, drawing the error once will only appear for a single frame // so create an event that keeps it on the screen for some frames Position pos = u->getPosition(); Error lastErr = Broodwar->getLastError(); Broodwar->registerEvent([pos,lastErr](Game*){ Broodwar->drawTextMap(pos, "%c%s", Text::White, lastErr.c_str()); }, // action nullptr, // condition Broodwar->getLatencyFrames()); // frames to run
If your resource depot is idle, but the command to train a new worker has failed, then the code above is triggered. As you can see from the comments, it takes the error that caused the train command to fail and draws it on the screen. Broodwar->drawTextMap(pos, "%c%s", Text::White, lastErr.c_str());
This is the line that does the text drawing. Notice that it's a different method from the one that we used to say "Hello world!" earlier. Broodwar->sendText() sends as if you typed something into chat, Broodwar->drawText() on the other hand, draws the text at a specific location on the map. So basically, if an error occurs, we take the position that that error occurred at, find what type of error it was, and then draw it on the screen at that position. Drawing things on the screen like this obviously doesn't help the AI play the game, but it's very useful for testing purposes so you can see what exactly is going wrong. The more time you spend developing your AI, the more stuff you will find you want to draw on the screen until you start running out of space. // Retrieve the supply provider type in the case that we have run out of supplies UnitType supplyProviderType = u->getType().getRace().getSupplyProvider(); static int lastChecked = 0;
// If we are supply blocked and haven't tried constructing more recently if ( lastErr == Errors::Insufficient_Supply && lastChecked + 400 < Broodwar->getFrameCount() && Broodwar->self()->incompleteUnitCount(supplyProviderType) == 0 ) { lastChecked = Broodwar->getFrameCount();
// Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned); // If a unit was found if ( supplyBuilder ) { if ( supplyProviderType.isBuilding() ) { TilePosition targetBuildLocation = Broodwar->getBuildLocation(supplyProviderType, supplyBuilder->getTilePosition()); if ( targetBuildLocation ) { // Register an event that draws the target build location Broodwar->registerEvent([targetBuildLocation,supplyProviderType](Game*) { Broodwar->drawBoxMap( Position(targetBuildLocation), Position(targetBuildLocation + supplyProviderType.tileSize()), Colors::Blue); }, nullptr, // condition supplyProviderType.buildTime() + 100 ); // frames to run
// Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation ); } } else { // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType ); } } // closure: supplyBuilder is valid } // closure: insufficient supply } // closure: failed to train idle unit
Now we have the section that deals with the construction of additional supply providers. Again, it's written in a way that means that it should work no matter what race the AI is playing. This means that the code is a little bit longer than if it only worked for one race, because obviously different races have different ways of providing supply. UnitType supplyProviderType = u->getType().getRace().getSupplyProvider(); Here we check the type of supply provider that we need. This is done by checking which race we are and then checking the type of supply provider that that races uses; this value is then stored in the variable supplyProviderType. So take Terran for example. We will take u, which is a pointer to one of our units, then check what type of unit (getType()) u is. Once we know the type, we check which race that type of unit belongs to (getRace()). Once we know the race, we can check what type of supply provider that race uses (getSupplyProvider()). Now we know the type of unit (UnitType) which we need to create, We then store this in the variable supplyProviderType so that we can use it later. // If we are supply blocked and haven't tried constructing more recently if ( lastErr == Errors::Insufficient_Supply && lastChecked + 400 < Broodwar->getFrameCount() && Broodwar->self()->incompleteUnitCount(supplyProviderType) == 0 ) { lastChecked = Broodwar->getFrameCount();
This code checks the type of error that we registered earlier. If it was an insufficient supply error then we are going to want to create more supply providers. However, remember that this method is called every frame. If we don't add in any additional checks, the AI would just spam as many supply providers as it could afford. This is because it would check "am i supply blocked?: yes" and queue one up, then 1 frame later it would again check "am i supply blocked?: still yes" and add an additional supply provider. It would keep doing this every frame until a supply provider was completed. So if we are a supply blocked zerg, on the first supply blocked frame, we will construct an overlord. Then before that overlord is even at 1%, we will construct another one, and we will keep doing that every frame that we have the money and larvae to do so until the first overlord is completed. Obviously we don't want that many overlords, so we need to prevent this from happening. ExampleAIModule handles this by recording the number of the frame that we last checked for supply block. If the time we last checked was less than 400 frames ago (roughly 17 seconds) then we will ignore the fact that we are still supply blocked, because we should already have something in production. // Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned); // If a unit was found if ( supplyBuilder ) { if ( supplyProviderType.isBuilding() ) { TilePosition targetBuildLocation = Broodwar->getBuildLocation(supplyProviderType, supplyBuilder->getTilePosition()); if ( targetBuildLocation ) { // Register an event that draws the target build location Broodwar->registerEvent([targetBuildLocation,supplyProviderType](Game*) { Broodwar->drawBoxMap( Position(targetBuildLocation), Position(targetBuildLocation + supplyProviderType.tileSize()), Colors::Blue); }, nullptr, // condition supplyProviderType.buildTime() + 100 ); // frames to run
// Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation ); } } else { // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType ); } } // closure: supplyBuilder is valid } // closure: insufficient supply
Finally, we have the code that deals with the actual creation of new supply providers. This is done by attempting to find a unit which can construct supply providers. If we find one then we can start constructing a supply provider. If no such unit exists, then we must be playing zerg, so we can train a new supply provider from our hatchery instead. // Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned);
When looking for someone to build our supply depot/pylon, we want to make sure that we are getting the right type of unit. Earlier, we stored the type of supply provider we need in supplyProviderType, now we are going to use it to check the type of unit that can construct it. This is done using supplyProviderType.whatBuilds().first. the whatBuilds() method returns a pair, where the first value is the type of unit and the second value is the number of those units required. In StarCraft you can only have 1 worker constructing a building, so the second value is usually 1; the only real exception to this is Archons I guess, which will return <Protoss_High_Templar, 2>. Now that we know that we are looking for an SCV or Probe, we need to check that we aren't selecting one that is already doing something important. We do this by making sure that the worker we select is either idle or mining minerals: we don't want to use our scouting worker or a worker already constructing a building to build the new supply provider. Ok so we have a builder, and we know what we want to build; now we just need a location. ExampleAIModule uses getBuildPosition(). This one is actually new to me; I'm used to BWAPI 3.7.4 so maybe it's a new feature, or maybe I just wasn't aware of it before. But when I started making an AI I spent ages messing around trying to make a system for finding suitable build locations; but it looks like now you can just call this method, as long as you don't care too much about the exact location of the building. The documentation says getBuildPosition(): Retrieves a basic build position just as the default Computer AI would. which doesn't sound too bad. My AI's method of finding building positions sometimes leads to it occidentally walling itself in. I might try testing this one instead of mine and see which is more effective. Anyway, now we have a build position too so we can construct that pesky supply provider. // Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation );
This is the bit that does the actual build command. supplyBuilder is our worker, and we are ordering him to build. The build command requires 2 values, the type of building and the location that we want to build it at. We stored the building type in supplyProviderType so we can use that, and we stored the result of getBuildPosition() in the variable targetBuildLocation, so we can use that. If we're zerg instead, then this is the line that does the overlord production: // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType );
In this case, supplyBuilder is our Hatchery rather than an SCV or Probe and we are going to train a unit rather than building a structure. The train command only requires one value, which is the type of unit to be trained, so we can give it supplyProviderType, which will be Zerg_Overlord. So hopefully now you have an idea of how the ExampleAIModule goes about automatically mining, training workers and creating additional supply providers. If we want an AI that does anything else, we're going to need to do it ourselves. Modifying the ExampleAIModule + Show Spoiler +An AI that just makes SCVs and supply depots isn't very interesting, so let's make it do something cooler! Let's make the AI perform a simple strategy; a 4 pool. 1. Building a spawning Pool So firstly, we need to make the AI build a spawning pool. We already have a loop that iterates through all our units, so we can use that to find a worker and construct the pool. //find a location for spawning pool and construct it TilePosition buildPosition = Broodwar->getBuildLocation(UnitTypes:erg_Spawning_Pool, u->getTilePosition()); u->build(UnitTypes:erg_Spawning_Pool, buildPosition);
In order to build the pool, we need to find a suitable location to build it. To do this, we can use Broodwar->getBuildLocation() which we learned about earlier. We want to build a Spawning pool so lets put that unit type into the first argument, and we want to build it near to where the drone currently is, so lets put the current tile position of the drone in the second argument (u->getTilePosition()). TilePositions are one of the 3 types of position that are used by BWAPI. The other two are Postion and WalkPosition. All 3 types are stored as a grid location which corresponds to a point on the map. Position is the smallest size and corresponds to a single pixel; WalkPosition is a square of size 8x8 pixels and TilePosition is a square of size 32x32 pixels. When units move around the map, they move from one WalkPosition to another. Building placement however is measured in terms of TilePositions. Position(0,0) corresponds to the pixel in the very to left corner of the map. Position (1,2) is shifted one pixel to the right and 2 pixels down. Even though our drone moves around in terms of WalkPositions, we can still find out which TilePosition it is currently occupying; this is done using u->getTilePosition() (remember u here is a pointer to our drone). Now that we have a location for the spawning pool, we can order our drone to construct it. This is done using u->build(). We now have code that finds a drone and tells it to make a spawning pool. However, there's several problems with this. The main problem is that, since this code is in onFrame(), it is going to be executed every frame. This means that every frame, we are going to tell every one of it's drones to build a spawning pool. We only want one spawning pool though, so we need a way to stop it from building more than one. We also don't check whether we have enough money to actually build the pool; so we are constantly spamming build commands even when we have no money. Making sure that we have enough minerals is fairly straight forward: if (Broodwar->self()->minerals() >= UnitTypes:erg_Spawning_Pool.mineralPrice()) { //build pool }
Before we attempt to build the pool, we should check how many minerals we have. This can be done using Broodwar->self()->minerals(). This returns the amount of minerals we currently have. We then want to compare this number against the cost of a spawning pool. You could just check if the minerals are >= 200, but it's usually better to avoid putting numbers in your code like this. We can check unit stats and costs by looking up their UnitType. We can find the cost of a UnitType by using UnitType.mineralPrice(). Next we need to make sure we only build 1 spawning pool. This means we only want to build a spawning pool if we have not already started building one. One way of doing this is by having a bool variable which can store the current spawning pool status. We then just need to check if we have started building a pool, and if we have we can change the bool to true. If the bool value is false then we haven't yet built a pool and we can try to build one, but if it's true then we already have one so we can skip trying to build one. I created a class variable called pool in ExampleAIModule.h. Next, in onStart(), we can initialise pool: pool = false. Then back in onFrame(), we can add another condition before attempting to build a pool: if (!pool && (Broodwar->self()->minerals() >= UnitTypes:erg_Spawning_Pool.mineralPrice())) { //build pool }
So now, as well as checking that we have enough money, we check that we don't already have a pool (if(!pool ...). Now we just need to check if we've started a pool. if ((!pool) && (Broodwar->self()->minerals() >= UnitTypes:erg_Spawning_Pool.mineralPrice())) { //find a location for spawning pool and construct it TilePosition buildPosition = Broodwar->getBuildLocation(BWAPI::UnitTypes:erg_Spawning_Pool, u->getTilePosition()); u->build(UnitTypes:erg_Spawning_Pool, buildPosition); pool = true; }
So now this is what the final code looks like. If we have enough money and haven't issued a build pool command before then we will tell a drone to build a pool. After that we set the value of pool to true so that we don't send any more unnecessary commands or tell multiple workers to do the same thing. 2. Making lings We already have a line which builds workers whenever we have idle larvae, but we don't want to build any workers because we're 4 pooling, so lets just change it to build zerglings instead. Both are produced from larvae at the hatchery so we don't really need to make any other changes. I also added in a condition to make sure that pool is true before we start attempting to build lings, just so it spams less commands. if (pool && (u->isIdle() && !u->train(UnitTypes:erg_Zergling) ))
Easy huh. 3. Attacking! Now our AI can successfully build a pool and start making lings, but we still need to attack with those lings. I'm afraid I'm going to wuss out a bit at this point and just enable the CompleteMapInformation flag. Turning on this flag gives the AI complete information about it's opponents units even through fog of war. Normally I would turn this flag off because it's against the rules for the AIIDE (and other) tournament. It's also less cool to have a bot with a maphack than one that knows how to scout. However, it seems like the library I used to use for map analysis ( BWTA) no longer works with the latest version of BWAPI; and I think coming up with our own solution to the problem of scouting is a bit beyond the scope of this guide so to simply things we're just going to leave that element out. If you come up with a way to scout then you can easily just turn the flag back off. Since we don't have to worry about finding our enemy, commanding our lings to attack becomes very simple: if ((u->getType() == UnitTypes:erg_Zergling) && u->isIdle()) { Unit closestEnemy = NULL; for (auto &e : Broodwar->enemy()->getUnits()) { if ((closestEnemy == NULL) || (e->getDistance(u) < closestEnemy->getDistance(u))) { closestEnemy = e; } } u->attack(closestEnemy, false); }
We can add this code into the loop in onFrame(). When looping through all of our units, if we find an idle zergling then we can issue an attack command. We still need to find a target though so I made a simple way of finding a target by finding the closest enemy. To do this we iterate through all of the enemy units and compare their distances from our zergling. We store the unit with the smallest distance in the closestEnemy variable. When we've checked all of the enemy units, we can then issue an attack command on whichever was closest. Our AI now builds a pool, trains lings and tells them to attack. Final Code: (Dll.cpp remains unchanged) ExampleAIModule.cpp + Show Spoiler + #include "ExampleAIModule.h" #include <iostream>
using namespace BWAPI; using namespace Filter;
void ExampleAIModule::onStart() { // Hello World! Broodwar->sendText("Hello world!");
// Print the map name. // BWAPI returns std::string when retrieving a string, don't forget to add .c_str() when printing! Broodwar << "The map is " << Broodwar->mapName() << "!" << std::endl;
// Enable the UserInput flag, which allows us to control the bot and type messages. Broodwar->enableFlag(Flag::UserInput);
// Uncomment the following line and the bot will know about everything through the fog of war (cheat). Broodwar->enableFlag(Flag::CompleteMapInformation);
// Set the command optimization level so that common commands can be grouped // and reduce the bot's APM (Actions Per Minute). Broodwar->setCommandOptimizationLevel(2);
// Check if this is a replay if ( Broodwar->isReplay() ) {
// Announce the players in the replay Broodwar << "The following players are in this replay:" << std::endl; // Iterate all the players in the game using a std:: iterator Playerset players = Broodwar->getPlayers(); for(auto p : players) { // Only print the player if they are not an observer if ( !p->isObserver() ) Broodwar << p->getName() << ", playing as " << p->getRace() << std::endl; }
} else // if this is not a replay { // Retrieve you and your enemy's races. enemy() will just return the first enemy. // If you wish to deal with multiple enemies then you must use enemies(). if ( Broodwar->enemy() ) // First make sure there is an enemy Broodwar << "The matchup is " << Broodwar->self()->getRace() << " vs " << Broodwar->enemy()->getRace() << std::endl; }
pool = false; }
void ExampleAIModule::onEnd(bool isWinner) { // Called when the game ends if ( isWinner ) { // Log your win here! } }
void ExampleAIModule::onFrame() { // Called once every game frame
// Display the game frame rate as text in the upper left area of the screen Broodwar->drawTextScreen(200, 0, "FPS: %d", Broodwar->getFPS() ); Broodwar->drawTextScreen(200, 20, "Average FPS: %f", Broodwar->getAverageFPS() );
// Return if the game is a replay or is paused if ( Broodwar->isReplay() || Broodwar->isPaused() || !Broodwar->self() ) return;
// Prevent spamming by only running our onFrame once every number of latency frames. // Latency frames are the number of frames before commands are processed. if ( Broodwar->getFrameCount() % Broodwar->getLatencyFrames() != 0 ) return;
// Iterate through all the units that we own for (auto &u : Broodwar->self()->getUnits()) { // Ignore the unit if it no longer exists // Make sure to include this block when handling any Unit pointer! if ( !u->exists() ) continue;
// Ignore the unit if it has one of the following status ailments if ( u->isLockedDown() || u->isMaelstrommed() || u->isStasised() ) continue;
// Ignore the unit if it is in one of the following states if ( u->isLoaded() || !u->isPowered() || u->isStuck() ) continue;
// Ignore the unit if it is incomplete or busy constructing if (!u->isCompleted() || u->isConstructing() ) continue;
// Finally make the unit do some stuff!
if ((u->getType() == UnitTypes:erg_Zergling) && u->isIdle()) { Unit closestEnemy = NULL; for (auto &e : Broodwar->enemy()->getUnits()) { if ((closestEnemy == NULL) || (e->getDistance(u) < closestEnemy->getDistance(u))) { closestEnemy = e; } } u->attack(closestEnemy, false); }
// If the unit is a worker unit if ( u->getType().isWorker() ) { if ((!pool) && (Broodwar->self()->minerals() >= UnitTypes:erg_Spawning_Pool.mineralPrice())) { //find a location for spawning pool and construct it TilePosition buildPosition = Broodwar->getBuildLocation(BWAPI::UnitTypes:erg_Spawning_Pool, u->getTilePosition()); u->build(UnitTypes:erg_Spawning_Pool, buildPosition); pool = true; }
// if our worker is idle if ( u->isIdle() ) { // Order workers carrying a resource to return them to the center, // otherwise find a mineral patch to harvest. if ( u->isCarryingGas() || u->isCarryingMinerals() ) { u->returnCargo(); } else if ( !u->getPowerUp() ) // The worker cannot harvest anything if it { // is carrying a powerup such as a flag // Harvest from the nearest mineral patch or gas refinery if ( !u->gather( u->getClosestUnit( IsMineralField || IsRefinery )) ) { // If the call fails, then print the last error message Broodwar << Broodwar->getLastError() << std::endl; }
} // closure: has no powerup } // closure: if idle
} else if ( u->getType().isResourceDepot() ) // A resource depot is a Command Center, Nexus, or Hatchery {
// Order the depot to construct more workers! But only when it is idle. if (pool && (u->isIdle() && !u->train(UnitTypes:erg_Zergling) )) { // If that fails, draw the error at the location so that you can visibly see what went wrong! // However, drawing the error once will only appear for a single frame // so create an event that keeps it on the screen for some frames Position pos = u->getPosition(); Error lastErr = Broodwar->getLastError(); Broodwar->registerEvent([pos,lastErr](Game*){ Broodwar->drawTextMap(pos, "%c%s", Text::White, lastErr.c_str()); }, // action nullptr, // condition Broodwar->getLatencyFrames()); // frames to run
// Retrieve the supply provider type in the case that we have run out of supplies UnitType supplyProviderType = u->getType().getRace().getSupplyProvider(); static int lastChecked = 0;
// If we are supply blocked and haven't tried constructing more recently if ( lastErr == Errors::Insufficient_Supply && lastChecked + 400 < Broodwar->getFrameCount() && Broodwar->self()->incompleteUnitCount(supplyProviderType) == 0 ) { lastChecked = Broodwar->getFrameCount();
// Retrieve a unit that is capable of constructing the supply needed Unit supplyBuilder = u->getClosestUnit( GetType == supplyProviderType.whatBuilds().first && (IsIdle || IsGatheringMinerals) && IsOwned); // If a unit was found if ( supplyBuilder ) { if ( supplyProviderType.isBuilding() ) { TilePosition targetBuildLocation = Broodwar->getBuildLocation(supplyProviderType, supplyBuilder->getTilePosition()); if ( targetBuildLocation ) { // Register an event that draws the target build location Broodwar->registerEvent([targetBuildLocation,supplyProviderType](Game*) { Broodwar->drawBoxMap( Position(targetBuildLocation), Position(targetBuildLocation + supplyProviderType.tileSize()), Colors::Blue); }, nullptr, // condition supplyProviderType.buildTime() + 100 ); // frames to run
// Order the builder to construct the supply structure supplyBuilder->build( supplyProviderType, targetBuildLocation ); } } else { // Train the supply provider (Overlord) if the provider is not a structure supplyBuilder->train( supplyProviderType ); } } // closure: supplyBuilder is valid } // closure: insufficient supply } // closure: failed to train idle unit
}
} // closure: unit iterator }
void ExampleAIModule::onSendText(std::string text) {
// Send the text to the game if it is not being processed. Broodwar->sendText("%s", text.c_str());
// Make sure to use %s and pass the text as a parameter, // otherwise you may run into problems when you use the %(percent) character!
}
void ExampleAIModule::onReceiveText(BWAPI:layer player, std::string text) { // Parse the received text Broodwar << player->getName() << " said \"" << text << "\"" << std::endl; }
void ExampleAIModule::onPlayerLeft(BWAPI:layer player) { // Interact verbally with the other players in the game by // announcing that the other player has left. Broodwar->sendText("Goodbye %s!", player->getName().c_str()); }
void ExampleAIModule::onNukeDetect(BWAPI:osition target) {
// Check if the target is a valid position if ( target ) { // if so, print the location of the nuclear strike target Broodwar << "Nuclear Launch Detected at " << target << std::endl; } else { // Otherwise, ask other players where the nuke is! Broodwar->sendText("Where's the nuke?"); }
// You can also retrieve all the nuclear missile targets using Broodwar->getNukeDots()! }
void ExampleAIModule::onUnitDiscover(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitEvade(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitShow(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitHide(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitCreate(BWAPI::Unit unit) { if ( Broodwar->isReplay() ) { // if we are in a replay, then we will print out the build order of the structures if ( unit->getType().isBuilding() && !unit->getPlayer()->isNeutral() ) { int seconds = Broodwar->getFrameCount()/24; int minutes = seconds/60; seconds %= 60; Broodwar->sendText("%.2d:%.2d: %s creates a %s", minutes, seconds, unit->getPlayer()->getName().c_str(), unit->getType().c_str()); } } }
void ExampleAIModule::onUnitDestroy(BWAPI::Unit unit) { }
void ExampleAIModule::onUnitMorph(BWAPI::Unit unit) { if ( Broodwar->isReplay() ) { // if we are in a replay, then we will print out the build order of the structures if ( unit->getType().isBuilding() && !unit->getPlayer()->isNeutral() ) { int seconds = Broodwar->getFrameCount()/24; int minutes = seconds/60; seconds %= 60; Broodwar->sendText("%.2d:%.2d: %s morphs a %s", minutes, seconds, unit->getPlayer()->getName().c_str(), unit->getType().c_str()); } } }
void ExampleAIModule::onUnitRenegade(BWAPI::Unit unit) { }
void ExampleAIModule::onSaveGame(std::string gameName) { Broodwar << "The game was saved to \"" << gameName << "\"" << std::endl; }
void ExampleAIModule::onUnitComplete(BWAPI::Unit unit) { }
ExampleAIModule.h + Show Spoiler + #pragma once #include <BWAPI.h>
// Remember not to use "Broodwar" in any global class constructor!
class ExampleAIModule : public BWAPI::AIModule { bool pool; public: // Virtual functions for callbacks, leave these as they are. virtual void onStart(); virtual void onEnd(bool isWinner); virtual void onFrame(); virtual void onSendText(std::string text); virtual void onReceiveText(BWAPI:layer player, std::string text); virtual void onPlayerLeft(BWAPI:layer player); virtual void onNukeDetect(BWAPI:osition target); virtual void onUnitDiscover(BWAPI::Unit unit); virtual void onUnitEvade(BWAPI::Unit unit); virtual void onUnitShow(BWAPI::Unit unit); virtual void onUnitHide(BWAPI::Unit unit); virtual void onUnitCreate(BWAPI::Unit unit); virtual void onUnitDestroy(BWAPI::Unit unit); virtual void onUnitMorph(BWAPI::Unit unit); virtual void onUnitRenegade(BWAPI::Unit unit); virtual void onSaveGame(std::string gameName); virtual void onUnitComplete(BWAPI::Unit unit); // Everything below this line is safe to modify.
};
And finally here's a link to the VS project with all the changes already made to it: http://www.mediafire.com/download/8e78e2q268easny/ExampleAIModule4pool.vcxprojTesting + Show Spoiler +Now that we've updated the ExampleAIModule to perform a sick 4 pool, we need to test that it works ok. We can do this by following the same steps we originally followed to compile and run the ExampleAIModule the first time. Compile, or 'build', the solution in Visual Studio, then copy the compiled dll file from the BWAPI/Releases/ folder into StarCraft/bwapi-date/AI. Then run StarCraft through ChaosLauncher. You should hopefully see the AI build a spawning pool, make some lings and attack. Here's a link to the final compiled AI (called 4pooler.dll): http://www.mediafire.com/download/2ak4edihtbfid06/4pooler.dllHere's a link to a replay of the SCBW default AI getting rekt by 4pooler: http://www.mediafire.com/download/8mealsa0a3xp0fz/4pooler.repFurther Development + Show Spoiler +Obviously, this is a very simple AI which would be really easy to beat for a human player. Some obvious weaknesses are: - It can't transition out of mining with 3 drones and attacking with lings. If it's opponent manages to defend the initial lings, it will just keep going and will never stop until one player or the other is dead. - It can't micro at all. The zerglings just attack the closest enemy and don't pay any attention to more important targets or retreat when they are going to die or anything like that. - It can't recover from losing a structure. If the AI's opponent came and killed the drone before it started building the pool, or if the pool died, then it wouldn't attempt to build another one because the pool variable would still be set to true. In this scenario, the AI would just stop doing anything, because it would be unable to train more zerglings. - It has CompleteMapInformation turned on. Ideally we need a solution to scouting our opponent so that the AI doesn't need to cheat. Even though our AI is only performing a simple strategy, there's still loads of ways that we could improve it in order to be more effective. If you check out some of the more successful AI systems (you can find bots and replays here) you will see that they are much more sophisticated than the crude 4 pool AI we just created. Some of them can scout and micro and are much more flexible than our AI. By flexible I mean that they are capable of adapting to their opponent or to unexpected situations. Some useful links: - AIIDE StarCraft Competition: http://webdocs.cs.ualberta.ca/~cdavid/starcraftaicomp/- CIG StarCraft Competition: http://cilab.sejong.ac.kr/sc_competition/- SCAII StarCraft Tournament: http://www.sscaitournament.com/- A Survey of Real-Time Strategy Game AI Research and Competition in StarCraft: http://webdocs.cs.ualberta.ca/~cdavid/pdf/starcraft_survey.pdf- BWAPI releases page: https://github.com/bwapi/bwapi/releases- BWAPI Documentation: http://bwapi.github.io/index.htmlHopefully this will be of interest to someone. I'm not really sure who this is aimed at or if the guide is too basic or too difficult. If you have any questions then I'm happy to answer them. If you just want to tell me I'm wrong and I suck then that's ok too I guess. You are a gentleman sir, please have all my respect and a cookie (you have to tell me which kind first though). Thank you for sharing. Broodwar editor is so fun to use, ai adds so much layers to it! thank you! thank you! thank you for your post!
On May 16 2015 09:19 KingAlphard wrote: Why do people only make custom AI for bw and not for sc2? They do too, but in all cases (in bw or in sc2, in "extra dev or like in making "simple" melee maps) there is a lot of work and a vital need for testers/users that will benefit/boost the work a "coder" (or coders) is capable of (not to mention it is tedious and you need support, which is almost totally nonexistent (less in bw than in others but still arguably a wasteland *insert sadface here*)).
|
On May 16 2015 08:39 2Pacalypse- wrote: This is a great post; thank you for writing it.
Spotlighted!
Thanks!
On May 16 2015 08:51 ejac wrote: Hmmm... hope you guys run this again next year, I'll join. I'm just starting to learn C and am proficient enough in Java.
I expect it will run again next year; I think the AIIDE tournament at least has been running since 2010, and there's a couple of other ones out there (CIG and SCAII). However, I think it's possible to do this using Java too if you would prefer it, although I have never tried so I can't really say how difficult it is to get started.
Here is a Java interface for BWAPI: https://github.com/JNIBWAPI/JNIBWAPI
On May 16 2015 09:19 KingAlphard wrote: Why do people only make custom AI for bw and not for sc2?
I think one of the main things is because BWAPI is basically a 'hack'. Blizzard puts quite a lot of effort into preventing people doing things like this for SC2 so it would be a lot more difficult to make something like BWAPI, and you'd probably get your account banned. They don't seem to mind or care that we this for BW though.
I'd love to be able to try to make an AI for SC2, but I think it would be way more difficult and I have no idea how to go about it really. BWAPI gives us a nice easy to use interface for interacting with BroodWar.
Also I found a FAQ on the BWAPI Github page:
Will there be an API like this for Starcraft II?
No. The BWAPI team is not interested in developing an API for [http://www.battle.net/sc2/ Starcraft II]. Some reasons are listed below. [http://www.blizzard.com Blizzard Entertainment] has a strict [http://www.blizzard.com/support/article.xml?tag=SC2exploitation anti-hacking policy] for [http://www.battle.net/sc2/ Starcraft II]. * Creating hacks and freely moving about the binary is far more difficult than doing so with [http://www.blizzard.com/games/sc/ Starcraft: Broodwar]. * The [http://www.battle.net/sc2/ Starcraft II] engine is not ideal for AI development. Genetic and learning algorithms will see almost no results in comparison. * AI/API developers will need to handle far more information than with BWAPI. * Embarking on such a project will require an exponentially greater amount of time to develop. * The [http://www.blizzard.com/support/article.xml?tag=SC2MINSPEC system requirements] are in another universe compared to those of [http://www.blizzard.com/support/article.xml?articleId=25801 Starcraft: Broodwar].
|
Go mooose! Brilliant write-up. Please also upload your bot to the SSCAIT server so we can see how it rolls!
|
if ((u->getType() == UnitTypes::Zerg_Zergling) && u->isIdle()) { Unit closestEnemy = NULL; for (auto &e : Broodwar->enemy()->getUnits()) { if ((closestEnemy == NULL) || (e->getDistance(u) < closestEnemy->getDistance(u))) { closestEnemy = e; } } u->attack(closestEnemy, false); }
Can be summed up as
if ((u->getType() == UnitTypes::Zerg_Zergling) && u->isIdle()) { u->attack(u->getClosestUnit(Filters::IsEnemy)); }
See UnitInterface::getClosestUnit and Filters::IsEnemy.
You can also generalize requirements for any type and have the AI build them by recursively calling UnitType::requiredUnits() and iterating that, while storing that you told a worker to construct it (and then going further to tracking workers that have been told to construct something to identify if they've been killed or couldn't reach the building site within a reasonable time).
|
Ah great, thanks! There seems to be a lot of stuff that I wasn't aware of; I need to read the documentation lol. Even going through the ExampleAIModule's code I learned a bunch of new things. I'd only used 3.7.4 before.
|
Very nice post, well done!
|
Been wanting to get into this for a long time. Great to see a compilation on how to get started. Good way to brush up on some C++. I just need to make time... TT
|
Belgium6753 Posts
This is awesome. I remember downloading the necessary stuff way back when, but I had just gotten into programming and C++ seemed way too daunting for some reason. Turns out it's not that hard after all, so I definitely think I'm going to give this a whirl again sometime soon. Really nicely written intro, thanks!
One thing I noticed: at first you mention getBuildPosition() but then you use getBuildLocation() in your code, unless I'm mixing something up?
|
Great tutorial. The BWAPI AI community really needs more contributions like this. Thanks!
|
On May 18 2015 04:59 Xeofreestyler wrote: This is awesome. I remember downloading the necessary stuff way back when, but I had just gotten into programming and C++ seemed way too daunting for some reason. Turns out it's not that hard after all, so I definitely think I'm going to give this a whirl again sometime soon. Really nicely written intro, thanks!
One thing I noticed: at first you mention getBuildPosition() but then you use getBuildLocation() in your code, unless I'm mixing something up?
Ah good catch, thanks. getBuildLocation() is correct; I don't think getBuildPosition() exists. I've edited the OP to remove references to 'getBuildPosition'. I seem to have a bad habit of using the words 'position' and 'location' interchangeably, which has led to a bunch of errors in my code before.
http://bwapi.github.io/class_b_w_a_p_i_1_1_game.html#a509aef285de00a0252be5816460b3325
|
On May 16 2015 08:51 ejac wrote: Hmmm... hope you guys run this again next year, I'll join. I'm just starting to learn C and am proficient enough in Java. ejac, SSCAIT tournament (http://sscaitournament.com/) runs 24/7 - you can enter any time and then submit new bot versions when you have them. And it's all streamed live. If you prefer Java, there's a java tutorial at http://sscaitournament.com/index.php?action=tutorial
|
nice !! ty mooose and Heinermann. Seems hard to strat but fun to do, i'll probably give it a try. ty again!!
|
So, generally do these AIs execute certain builds or play adaptively like chess AIs which execute certain moves based on win percentage rather than a "game plan"? if it's builds are they mostly rushes? I'd imagine it gets pretty complex the longer the game goes
|
|
|
|