|
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'. ![[image loading]](http://i.imgur.com/CxCysuD.png) It's here. 6. In the Solution Explorer window, right click on ExampleAIModule and click 'Build'. ![[image loading]](http://i.imgur.com/gQGVvPY.png) 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. ![[image loading]](http://i.imgur.com/gTWGgPm.png) 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. ![[image loading]](http://i.imgur.com/hZSpPnh.png) 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. ![[image loading]](http://i.imgur.com/sHr6xg9.png) 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!
|
Croatia9489 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'. ![[image loading]](http://i.imgur.com/CxCysuD.png) It's here. 6. In the Solution Explorer window, right click on ExampleAIModule and click 'Build'. ![[image loading]](http://i.imgur.com/gQGVvPY.png) 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. ![[image loading]](http://i.imgur.com/gTWGgPm.png) 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. ![[image loading]](http://i.imgur.com/hZSpPnh.png) 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. ![[image loading]](http://i.imgur.com/sHr6xg9.png) 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
|
Belgium6768 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
|
On May 19 2015 08:47 Lobotomist wrote: 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
Some of the AIs, my one included, are capable of playing adaptively to a certain degree, but I don't think any of them calculate the best possible move like a Chess AI would. The range of possible moves in StarCraft is enormous; there's a much larger number of pieces on the board than Chess, every piece is capable of moving simultaneously, and it's possible to create new pieces. Then you add in the fact that you have up to 55ms to calculate before being disqualified and the Chess style solution isn't really workable.
The focus of my AI last year was to be reactive in terms of unit composition. I based it around David Churchill's StarCraftBuildOrderSearch which was taken from his UAlbertaBot AI. This allows you to input a goal set of units, for example 10 Marines and 1 Siege Tank. It then takes into account the current game state such as your resources, worker count, tech level and current production capacity and calculates a viable build order that would result in the creation of the goal set of units. So if I already have a few barracks and a factory and a healthy worker count, and I put in the previous example goal set of units, the outputted build order would probably just be to train marines and a tank and build enough supply depots to accommodate those units. However if, say, I don't have a factory, then the build order will include the construction of a factory before it attempts to train any tanks.
To determine the build order goals to feed into the build order search, my AI would collect information about it's opponent. It would store the number and type of all enemy units that it had seen, and translate these into a percentage. So if it sees 8 Mutas and 12 lings, the enemy composition would be 40% Muta, 60% Zergling. It then had a table of enemy unit compositions and compositions that were 'counters' to them. It would search through the table for the most similar enemy composition to the current one and then retrieve the 'counter' unit composition and use that as a build order goal to feed into the build order search. The build order that was outputted by the build order search could then be executed by the lower level parts of the AI system that manage building placement and unit training and stuff.
So what I'm saying is that, in a limited way, my AI was capable of scouting it's opponent and reacting to its unit composition. If there were no similar enough unit compositions in the table, it was also capable of slightly adapting a previous one to fit a new situation, and if the new composition was successful in the game, it would store it in the table for future use. I found that last part wasn't particularly effective though, maybe I'll get round to improving it this year.
Since I've switched my AI to zerg this year, I had problems with the build order search module I was using, so I have replaced it with my own one. My one is pretty crude at the moment, but it's sort of working ok.
A major weakness from last year was that reacting in terms of unit compositions is only one of the factors a StarCraft player needs to pay attention to. My AI wasn't capable of comparing its economy to its opponent's, so sometimes it would be sitting on one base trying to concoct the perfect composition while it's opponent would be macroing on 3 bases. It doesn't really matter what type of unit you build if you only have 1/3rd of your opponent's economy. This year I'm trying to make my AI capable of adapting in other meaningful ways.
I think some of the AI systems just follow a scripted build order throughout the game, which can be pretty effective because at least the build order can be optimised by the developer. My AI last year would often come out with the most ridiculous build orders (3 robo observer in reaction to a single lurker).
|
Belgium6768 Posts
I was watching the stream, are those games live? They seemed to be running at 2x speed, is that correct? I read somewhere that your bot can't take more than 55ms per frame. Wouldn't this then be impacted by the higher framerate?
I think some of the AI systems just follow a scripted build order throughout the game, which can be pretty effective because at least the build order can be optimised by the developer. My AI last year would often come out with the most ridiculous build orders (3 robo observer in reaction to a single lurker).
hahaha brilliant. An obvious solution would be to just simply hard-code 'no multiple buildings of kind x'. You mentioned the table for future use. If your bot would do enough 'training sessions', wouldn't it figure out that this build sucked? It doesn't really teach it conceptually why it sucks but at least it seems like experience/learning is a more elegant solution to me. Actually letting a cpu conceptually understand this game is pretty much the million dollar question I guess.
Has anyone done any research towards using convolutional & recurrent neural nets in regards to tactical decisions (or perhaps even strategy?) I'm by no means an expert on this, I just know they're used for all sorts of computer vision tasks and abstracting patterns. So it doesn't seem too far out to apply the idea to analyzing (enemy) unit formations.
|
On May 19 2015 21:26 Xeofreestyler wrote:I was watching the stream, are those games live? They seemed to be running at 2x speed, is that correct? I read somewhere that your bot can't take more than 55ms per frame. Wouldn't this then be impacted by the higher framerate? Show nested quote +I think some of the AI systems just follow a scripted build order throughout the game, which can be pretty effective because at least the build order can be optimised by the developer. My AI last year would often come out with the most ridiculous build orders (3 robo observer in reaction to a single lurker). hahaha brilliant. An obvious solution would be to just simply hard-code 'no multiple buildings of kind x'. You mentioned the table for future use. If your bot would do enough 'training sessions', wouldn't it figure out that this build sucked? It doesn't really teach it conceptually why it sucks but at least it seems like experience/learning is a more elegant solution to me. Actually letting a cpu conceptually understand this game is pretty much the million dollar question I guess. Has anyone done any research towards using convolutional & recurrent neural nets in regards to tactical decisions (or perhaps even strategy?) I'm by no means an expert on this, I just know they're used for all sorts of computer vision tasks and abstracting patterns. So it doesn't seem too far out to apply the idea to analyzing (enemy) unit formations.
more than 1 million dollars, my friend, a lot more^^ An "AI" will never know if a build "sucks"^^. It'll required a input in order to provoke a reaction countering the craziness of the AI
In my opinion based on an average maths lvl for a nerd, i think RNN will be useless as the number of bifurcation can be huge in one game. However there is so many type of RNN so..... we need an expert :p
|
Damn, very interesting. Seems like a lot of rushes would be difficult to stop, in particular anything that requiresa worker pull. Or things that require inference (ie opponent is mining lots of gas but no tech buildings / units are seen). While creating a reactive ai that "plays" is certainly a more interesting challenge, i would think an ai that executes difficult-to-defend rushes would be more successful. Thoughts?
|
On May 19 2015 21:26 Xeofreestyler wrote: hahaha brilliant. An obvious solution would be to just simply hard-code 'no multiple buildings of kind x'. You mentioned the table for future use. If your bot would do enough 'training sessions', wouldn't it figure out that this build sucked? It doesn't really teach it conceptually why it sucks but at least it seems like experience/learning is a more elegant solution to me. Actually letting a cpu conceptually understand this game is pretty much the million dollar question I guess.
My bot's learning abilities were pretty shitty so no it wouldn't have. If it had had success executing a build in the past then it would've tried it again in future. It's possible that it could win a game even with a retarded build (say it's opponent bugs out or something) and then think that this build is ok to use again. Using some kind of learning mechanism would be a really nice feature if done well though (mine was not at all effective so I removed it from the AI for the competitions).
On May 20 2015 08:35 Lobotomist wrote: Damn, very interesting. Seems like a lot of rushes would be difficult to stop, in particular anything that requiresa worker pull. Or things that require inference (ie opponent is mining lots of gas but no tech buildings / units are seen). While creating a reactive ai that "plays" is certainly a more interesting challenge, i would think an ai that executes difficult-to-defend rushes would be more successful. Thoughts?
I think you're right; in StarCraft it sometimes requires more skill to see a rush coming and stop it than it does to execute it. I think these situations usually require a more sophisticated AI too. It isn't really a 'rush' but UAlbertaBot scouts after it's first pylon, when it finds the enemy base it starts attacking one of their workers with it's scouting probe. If it gets attacked then it runs away from the threat; running round in circles round the enemy base. If it stops being attacked then it comes back and starts attacking enemy workers again.
This is a really strong opening because it basically weeds out an AIs that don't have a system in place to deal with pulling workers. If your AI simply does nothing until it gets its first fighting unit out, you will probably have lost half of your workers by the time you get rid of the probe. If you have a simple response like "If I have no fighting units, pull all my workers and defend", then you end up with all your workers chasing a single probe around in circles. Again you will lose in this situation because you won't be mining. So in order to be able to defend this single probe harassment, you have to program your AI to be able to judge the threat level of the enemy units that are attacking and defend with a proportional force. Another method could be to return workers to mining if they chase too far or for too long. Either way it forces you to put in a bit of effort in development just to defend against 1 probe.
There's lots of possible little annoying things like this you can do in a StarCraft game I think. It's pretty easy for a human player to work out what's going on and not do something stupid like pull all their workers, but it's maybe not so simple for an AI. There's loads of little things you have to worry about, and it would be impractical to try to hard-code a response to every single possibility.
|
Belgium6768 Posts
I'm assuming the sscait tournament system can be used to let your bot play thousands of sessions, wouldn't this be sufficient to weed out retarded strategies? Of course the opponent needs to be somewhat capable as well, but the other bots are all open-source now, right? So why not simply brute force the approach like this until it has found optimal solutions?
Again, not an expert at all, just thinking out loud here
|
Thanks! I'd love to take a summer to develop a competitive AI (not that I think I'd be very successful). Maybe I can work a piece of it into a graduate school project someday ha.
|
The problem with algorithms that sort of "learn" is that they need thousands if not hundred thousands of samples to learn a small step. Well you say that's how we learn too. However that's not true, as we humans see a problem we take it and make a abstract version of that problem and make thousands calculations within our heads without playing or solving the problem for real. Until AI's are capable of this there is no way we'll see them to make big learning steps.
So scripted bots for the win.
|
On May 21 2015 07:19 LastWish wrote: The problem with algorithms that sort of "learn" is that they need thousands if not hundred thousands of samples to learn a small step. Well you say that's how we learn too. However that's not true, as we humans see a problem we take it and make a abstract version of that problem and make thousands calculations within our heads without playing or solving the problem for real. Until AI's are capable of this there is no way we'll see them to make big learning steps.
So scripted bots for the win.
If you're interested on the topic, you should read the last article of Google, Deep Mind on DNQ. The AI "learns" games!! not like chest when it's just basic computing. Humans dont make thousands of calculations to solve problem. If we could, there would not have been any need for counting frame^^ Basically, we just use memories to extrapolate alternative solutions.
SKYNET!!!! + Show Spoiler +
|
Belgium6768 Posts
Could you make a bot that learns in some way, upload it to one of those scai tournament websites and keep the memory synced with your version of the ai? Or does it have to be disconnected from internet?
|
Awesome, wanted to give BWAPI a shot for a long time, that should come in super handy.
|
there was an AI bw competition like 7 years a go
can someone link that?
|
On May 24 2015 04:49 repomaniak wrote: there was an AI bw competition like 7 years a go
can someone link that?
2010 AIIDE and CIG started, SSCAIT in 2011, but they are not consistently documented. I gathered the tournament websites a couple of months ago, in the links above.
|
On May 22 2015 06:46 Xeofreestyler wrote: Could you make a bot that learns in some way, upload it to one of those scai tournament websites and keep the memory synced with your version of the ai? Or does it have to be disconnected from internet?
Like Cazimirbzh said, it is a bit of a pain to learn complex behaviour if your learner (bot) is very artificial (hasn't got billions of years of evolution coded into it to learn or evolve). That said, you could play m/b/z/illions of games, the AIIDE tournament software is open source, ideally you'd need to make it faster so it could be played on a super cluster and reintegrate the results of that.
The oldest bots on SSCAIT have been there for years and played only about 4600 matches each. Compared to the number of variables involved that is not very much. Also most of these older bots have undergone major revisions, I doubt any current version has 2000 matches under its belt.
|
Hey, does anyone who's used this know how to compile an AI from source? I would really prefer not having to get Visual Studio, and it seems like it shouldn't be that hard with only 3 files in the VS project (though there may be a lot of interactions I'm missing from just looking at the files).
|
I think it should work with Visual Studio Express, which afaik is free.
|
On May 25 2015 13:06 IamTheArchitect wrote: Hey, does anyone who's used this know how to compile an AI from source? I would really prefer not having to get Visual Studio, and it seems like it shouldn't be that hard with only 3 files in the VS project (though there may be a lot of interactions I'm missing from just looking at the files).
On May 26 2015 21:24 PoP wrote: I think it should work with Visual Studio Express, which afaik is free.
Really? 
IamTheArchitect , what's your OS ? Also do you intend to use the software for others things apart from AI compiling ? I start by suggesting SharpDevelop.
User was warned for this post
|
afaik every solution file is cross compatile.
writing an AI seems a really cool thing to do, maby it I will start someday to get some dust off my c++ skills.
|
there wouldn't happen to be golang bindings for this would there?
|
I'm afraid I've only compiled using VS so I can't really help there.
Afaik there's no golang bindings either, tried googling and didn't find anything anyway :p
On May 25 2015 01:38 nepeta wrote:+ Show Spoiler +On May 22 2015 06:46 Xeofreestyler wrote: Could you make a bot that learns in some way, upload it to one of those scai tournament websites and keep the memory synced with your version of the ai? Or does it have to be disconnected from internet? Like Cazimirbzh said, it is a bit of a pain to learn complex behaviour if your learner (bot) is very artificial (hasn't got billions of years of evolution coded into it to learn or evolve). That said, you could play m/b/z/illions of games, the AIIDE tournament software is open source, ideally you'd need to make it faster so it could be played on a super cluster and reintegrate the results of that. The oldest bots on SSCAIT have been there for years and played only about 4600 matches each. Compared to the number of variables involved that is not very much. Also most of these older bots have undergone major revisions, I doubt any current version has 2000 matches under its belt.
I think if you really wanted to, you could run automated games faster than they do at the SSCAIT tournament. Last year the AIIDE tournament went on for like a week (iirc) and all the bots played 1139 games each: results.
I think the main difficulty is creating an effective learning mechanism that will actually benefit from grinding out thousands of games. I know my feeble attempt from last year wouldn't have.
|
Belgium6768 Posts
I'm not really interested in hard-coding behavior though. Right now this seems to be a bit of a rock-paper-scissors type of deal.
Have there been any attempts (even badly performing ones) that used some sort of ALife or emerging behavior technique? I'd rather just do some experiments in that and see what happens. I'm not interested in winning the ai tourney or anything like that :p (for the time being at least)
|
On May 27 2015 21:43 Xeofreestyler wrote: I'm not really interested in hard-coding behavior though. Right now this seems to be a bit of a rock-paper-scissors type of deal.
Have there been any attempts (even badly performing ones) that used some sort of ALife or emerging behavior technique? I'd rather just do some experiments in that and see what happens. I'm not interested in winning the ai tourney or anything like that :p (for the time being at least)
You'll need a lot of processing to run it and *2 (to write and to test). I dont think the broodwar community is enough^^. I share processing power (BOINC) and we barely reach 10 petaFLOPS.
|
Belgium6768 Posts
Damnit... ahead of my time again I see. My plans foiled by our primitive hardware! ... uh yeah ... that's the only reason why my plan wouldn't work.
And whoa, I've heard about BOINC before! That's some great work you're doing dude. I had an idea once a long time ago about how it would be interesting to create some sort of worm/virus that silently uses processing cycles for problems like protein folding and stuff. This is of course a better and on top of that legal way of doing that :D
Honestly I don't even know how to visualize what 10 petaFLOPS can do, any analogy you can use to explain?
|
You don't necessarily need to use hard coded behaviour in all areas of the bot though. The great thing about scbw is that it's such a complicated game with loads of areas you can develop. You might not be able to come up with a learning algorithm for playing the whole game, but you can break the problem into areas and develop one of them. For example build order planning or micro or scouting.
So for example you can't really use a search algorithm to determine all the moves in a StarCraft game because it's too complicated, but you can take one area of the game and apply one there. For example SparCraft. My bot lost to UAlbertaBot last year most of the time even though they were usually executing the same build order at each other. This was probably mostly due to UAlbertaBot using SparCraft and therefore having superior micro. So it's not really just about coming up with some stupid rock paper scissors build order and hoping it works. There's lots of different areas you can develop.
Also I updated the OP to include a reference to UAlbertaBot's github page since I've referred to it a bunch of times in this thread. It was also very useful for building my own system (I copied/stole a bunch of stuff).
|
Belgium6768 Posts
Good point. SparCraft looks really interesting! So if I understand correctly, a bot can use this in realtime to see whether it would win a fight or not?
|
On May 28 2015 10:55 Xeofreestyler wrote: Damnit... ahead of my time again I see. My plans foiled by our primitive hardware! ... uh yeah ... that's the only reason why my plan wouldn't work.
And whoa, I've heard about BOINC before! That's some great work you're doing dude. I had an idea once a long time ago about how it would be interesting to create some sort of worm/virus that silently uses processing cycles for problems like protein folding and stuff. This is of course a better and on top of that legal way of doing that :D
Honestly I don't even know how to visualize what 10 petaFLOPS can do, any analogy you can use to explain?
To understand what kind of hardware you need : "Intel Celeron G1830 and AMD Radeon R9 295x2 tops out at over 11.5 TFLOPS at a grand total of US$902.57" http://www.tomshardware.com/reviews/radeon-r9-295x2-review-benchmark-performance,3799.html http://www.freezepage.com/1420850340WGSMHXRBLE So if you had 1k of this pc lineup and they're all running for 1 second u have 11.5 PFLOPS^^ But it's actually even more harder to generate computing power. To compare, we have approximatly 500k pc link to boinc. ^^
It's really hard to explain how much is 10 PFLOPS  I tried to find a small project run by WCG (^^ World community Grid) to give a example. "Computing for Sustainable Water study the effects of human activity on a large watershed, Chesapeake Bay, and gain deeper insights into what actions can support the restoration, health and sustainability of this important water resource Stats: http://www.worldcommunitygrid.org/stat/viewProject.do?projectShortName=cfsw It required 4 PFLOPS in order to finished the job. At the opposite, the folding@home(the big one^^BOINC/google/sonic) project "Its current performance of 40.2 x86 petaFLOPS and 14.4 native petaFLOPS" And if you want to be amaze i suggest checking the total flops generated by boinc projects it's insane^^
I think BOINC is a good way to keep my pc usefull when he's idle because i am still paying the bill. Even when it does nothing^^. After 15 minutes of inactivity, and if there is no software running , my sceensaver switchs between SETI/Milkyway/Einstein/rosetta. I think it should be install BY LAW on all pc :p It's impossible to hack this kind of grid. The segmentation of the data is really impressive. Every 30m/1hour, you switch from a task to another. Also the distribution method prevent to build an emerging pattern. It's safer than bitcoin. The real security issue come from the details of your hardware and also personnal data you share.
Please, mooose, more stories. I am unable to grasp how many variables you need in order to have a AI that could play vs parting :p
|
I remember when putting actual code into Starcraft's AI was a pipedream. I remember all 10 of the years I spent mastering the ins and outs of how the hardcoded crap behaved and how to try to work around its innumerable quirks and annoyances, often fruitlessly, trying to get AI to work for my total conversions and pet projects. When I set down ZONS and thought I had created what would be the best Zerg AI for the game (later ZAPOC, but by then BWAPI had been in development for a short time and I knew the era of scaiedit was at an end). Still, the 8 hour duel between Mike's protoss and ZAPOC has a special place in my heart.
That was the time for me to finally step out of modding for good, because I'm no programmer despite my experience in modding. I wish I had taken a hint back then instead of wasting my time with other games for so long.
|
On May 28 2015 17:31 mooose wrote:You don't necessarily need to use hard coded behaviour in all areas of the bot though. The great thing about scbw is that it's such a complicated game with loads of areas you can develop. You might not be able to come up with a learning algorithm for playing the whole game, but you can break the problem into areas and develop one of them. For example build order planning or micro or scouting. So for example you can't really use a search algorithm to determine all the moves in a StarCraft game because it's too complicated, but you can take one area of the game and apply one there. For example SparCraft. My bot lost to UAlbertaBot last year most of the time even though they were usually executing the same build order at each other. This was probably mostly due to UAlbertaBot using SparCraft and therefore having superior micro. So it's not really just about coming up with some stupid rock paper scissors build order and hoping it works. There's lots of different areas you can develop. Also I updated the OP to include a reference to UAlbertaBot's github page since I've referred to it a bunch of times in this thread. It was also very useful for building my own system (I copied/stole a bunch of stuff).
BWAI should really be made modular, because of this. There are so many fields in which improvements can and have been made, but often new coders are reinventing the wheel. There are some open bots, but ideally there should be a modular structure which can absorb different versions of different fields like scouting, mining, attack-micro, but also more complex code for balancing eco vs army. Then if a module gets written, or improved, it can be incorporated in existing AIs.
|
I think a lot of the AI systems are modular actually. Mine is, although the architecture is a bit of a mess at this point. I should really go through and refactor before I develop it any further. Using UAlbertaBot as an example once again; it has a quite nice modular design that would allow you to replace specific parts with your own system if you wanted to only develop a specific module rather than an entire AI. For example, the bot treats the build order generator like a black box so if you came up with a better one you could probably replace the existing one without too much work.
|
Note: just observing, not really a programmer/ai genius
How do you fix the problem of AI's being eternally horrible at judgement? A lot of the time they have horrible misjudgement problems and often don't know when to stop or to continue committing to something, an AI protoss can fight into tanks and then retreat 2 seconds later, when continuing attacking would have been the less worse option.. A common form of this is having a protoss AI eternally chase 1 zergling with all zealots the protoss has. It could also completely go for the wrong amount of unit, let's say too many dragoons or too many carriers, because the AI often can't weigh the information it gets.
|
Belgium6768 Posts
Good question. I'm just gonna think out loud about it here, like an exercise, feel free to correct me.
Say you're using SparCraft. If I understand correctly, it uses a simplified model of how fights happen in bw so that an AI can 'predict' what the outcome of a battle will be. Since it ignores pathfinding and stuff (splash?), it's gonna be wrong a certain percent of the time (especially towards bigger and complex battles). Say your prediction gives a win, but suddenly the AI re-evaluates the situation as it is and it turns out you're losing the battle. SparCraft calculates how your squad is weaker than the other's, so the 'squad-manager' module orders your units to retreat (but they die to tank fire).
Now, retreating isn't necessarily always bad. Sometimes you can save a few units if the opposing squad doesn't have long range, meaning they can't kill you while you're running away. In your example, this is almost never going to happen since tanks have an enormous range and will still get a lot of kills while units run away. But in the case of zealots or something it wouldn't be bad to run away with your leftover lings if they have no way of winning.
So this leads me to think that the decision to either 'retreat' or 'suicide but do max damage' has to be a function of the opponent's range. I wouldn't know how to do the math for this though.
Perhaps a solution could be to assign aggressiveness levels to squads. 0 being retreat to safety immediately and n (max) being fight until the death. Somewhere in between there could be varying levels of harassment & retreating (positioning). Dunno if this actually makes it easier though, since you still have to figure out how to assign these levels to squads/units
|
On June 02 2015 18:10 Xeofreestyler wrote:Good question. I'm just gonna think out loud about it here, like an exercise, feel free to correct me. Say you're using SparCraft. If I understand correctly, it uses a simplified model of how fights happen in bw so that an AI can 'predict' what the outcome of a battle will be. Since it ignores pathfinding and stuff (splash?), it's gonna be wrong a certain percent of the time (especially towards bigger and complex battles). Say your prediction gives a win, but suddenly the AI re-evaluates the situation as it is and it turns out you're losing the battle. SparCraft calculates how your squad is weaker than the other's, so the 'squad-manager' module orders your units to retreat (but they die to tank fire). Now, retreating isn't necessarily always bad. Sometimes you can save a few units if the opposing squad doesn't have long range, meaning they can't kill you while you're running away. In your example, this is almost never going to happen since tanks have an enormous range and will still get a lot of kills while units run away. But in the case of zealots or something it wouldn't be bad to run away with your leftover lings if they have no way of winning. So this leads me to think that the decision to either 'retreat' or 'suicide but do max damage' has to be a function of the opponent's range. I wouldn't know how to do the math for this though. Perhaps a solution could be to assign aggressiveness levels to squads. 0 being retreat to safety immediately and n (max) being fight until the death. Somewhere in between there could be varying levels of harassment & retreating (positioning). Dunno if this actually makes it easier though, since you still have to figure out how to assign these levels to squads/units 
Basicly, you're right. If your IA is stuck you need to add more datas, functions to your script. Like DinosaurPoop said, "AI often can't weigh the information it gets." So you pump more usefull intel in order to conteract the loop.
|
Belgium6768 Posts
Ah it seems like I misunderstood SparCraft. It doesn't calculate the battles real-time, it does it in advance and then looks the results up in a database, correct? Sort of like endgame tables for chess?
So the problem is rather that either the database can be incomplete, or incorrect. On the one hand there's stuff like collision detection and splash which is probably not accounted for. But on the other hand there are unknown variables such as the opponent's micro skill. This isn't easy to solve indeed.
Hmm ... wouldn't a neural net be good at this type of statistical calculations?
|
On June 03 2015 20:44 Xeofreestyler wrote: Ah it seems like I misunderstood SparCraft. It doesn't calculate the battles real-time, it does it in advance and then looks the results up in a database, correct? Sort of like endgame tables for chess?
So the problem is rather that either the database can be incomplete, or incorrect. On the one hand there's stuff like collision detection and splash which is probably not accounted for. But on the other hand there are unknown variables such as the opponent's micro skill. This isn't easy to solve indeed.
Hmm ... wouldn't a neural net be good at this type of statistical calculations?
I have no idea about sparcraft but standalone and implemented into a BWAPIbased seems to be able to do both. We need our expert^^ ANN can help only when the unknown variables are known and coded^^ You have to consider it like the last level for bots. For this step, it required first to have the basics covered ( no army running after 1gling, no weird retreat, etc). ANN is required when you want to determine what's are the "hidden" reasons of a choice. It's like building a labyrinth and explore it after to draw the map in order to know where are the dead ends. :p ANN will allow you do input "simple" stuffs for very good results but the data will you have to feed and the nonnegative matrix you must compile in order to prevent crazy things is more than hardcore. I think, (where's our expert^^), at the moment, we're at the first step. We're gathering basics data. Next one will be to run simulations on a large scale (boinc) to have the data required for last step, ANN. At this point, it'll be not game per game but for Bo(x). It'll be very interesting to see what hint could/should be noticed during a previous game to select A or B build after a win or a lose.
edit : sc2replaystats is working on tools in order to help player but they're making good steps to gather data.
|
Belgium6768 Posts
Actually now that I think about it, quantifying an opponent's micro skill shouldn't be too difficult. Some sort of efficiency calculation by measuring total damage output per unit should give adequate results, no? Maybe measure how long it takes on average to kill an enemy's units, could be a good indicator too?
By measuring this during the game you could let the ai adapt and be less aggressive etc.
|
It'll be easy to calculate for an AI but for a human it's "impossible" too many variables (micro vs macro, epm vs apm, key units). However with an harass script designed to test apm, the AI could narrow the skill of a player and adapt his strategy according to the results. But it'll not be accurate because you'll just have to play bad during the harassscript to fool the AI.
|
Belgium6768 Posts
Hmm ... this sounds like a statistics problem though, I mean, isn't there a way to constantly update this calculation and leave out the worst performance out of the averaged equation? (in case of the harass script being 'fooled')
Cuz they can't micro badly all game right? I mean, then they'd just lose hahah
|
On June 05 2015 21:43 Xeofreestyler wrote: Hmm ... this sounds like a statistics problem though, I mean, isn't there a way to constantly update this calculation and leave out the worst performance out of the averaged equation? (in case of the harass script being 'fooled')
Cuz they can't micro badly all game right? I mean, then they'd just lose hahah Yes but we're reaching our current limit This apm calculation issue is a good example of what kind of systematic errors by lack of data a statistical approch will lead to. Because even with a harass script, a lot of scouting, the constant nature of apm data makes it impossible to predict. So, for a next engagement, in terms of effectiveness, unless it's vs AI with an "absolute effectiveness" script, i dont see how you can predict accurately an apm variable as a rule of engagement.
|
Belgium6768 Posts
Ok ok new idea!!
float p = random if < .5 retreat else attack
There, problem solved. 
No but really, I read that one bot has like a random 'mood' right? agressive/campy/cheesy/... I think this is a nice idea, keeps your bot from being too predictable.
|
Hi Mooose, that's a great write up. I had no idea that Broodwar AI competitions were still such a healthy scene. I'm writting my own AI for SC2 currently. Here is the last video of my progress I made.
Unfortunately no such API exists (or will likely ever exist) for SC2 so having a competitive scene would be near impossible. But looking at what is going on in the broodwar world makes me wish I had started there instead. Fighting other AIs would make for a great way to benchmark and rate your progress.
GL, I hope your AI does well
|
haha XD Yea for the moment, better to stop AI to go wild, even if it can be funny. And there is so much more to be done before that 
@turtles You should ask sc2replaystats; They're seems to develop tools for sc2. I think you clearly underestimate the number of pple who will watch a " resist our protoss AI" :p
|
Please help! I have tried to follow the instruction above but when I test it with ExampleAIModule.dll it doesn't work. And I have check the config file in Chaoslauncher and the path is correct. I am currently using starcraft BW 1.16.1 and BWAPI verson 4.2
|
can you make it so the AI responds to things in its vision? for example:
if (you see 12 or more dragoons AND time is less than 10 minutes) { retreat unit group X to location Y }
or something along these lines
|
On January 14 2017 14:34 forfeit1 wrote: Please help! I have tried to follow the instruction above but when I test it with ExampleAIModule.dll it doesn't work. And I have check the config file in Chaoslauncher and the path is correct. I am currently using starcraft BW 1.16.1 and BWAPI verson 4.2
Sorry for the slow reply I haven't been on here in a while. Are you still having problems?
On January 26 2017 23:38 mishimaBeef wrote: can you make it so the AI responds to things in its vision? for example:
if (you see 12 or more dragoons AND time is less than 10 minutes) { retreat unit group X to location Y }
or something along these lines
Sure yeah you could. My AI would keep track of the amount of enemies it had seen and compare it to the size of its own army and retreat if the opponents army was stronger. My one was pretty rudimentary though; I'm sure some of the other AI's have a much more sophisticated system.
You could make it count the number of visible enemies within a certain radius of your units and then issue commands based on that.
It's probably best to avoid hard coding things like your dragoon example though. You'd have to write a huge number of if statements to cover all the possible openings and responses.
|
|
|
|