Free and Open Source real time strategy game with a new take on micro-management

Making An AI With Echo (part 1)

From Globulation2

Revision as of 05:35, 11 July 2006 by Genixpro (talk | contribs)
Jump to: navigation, search

Introduction

Echo is the new subsystem for AI's. While Echo does not replace all of the existing AI system, it does provide a new interface to Glob2, and a large set of tools for making AI's. It can save allot of time, you would understand if you've made an AI without Echo.

You should read New AI System to get a general view of how Echo works before proceeding. You can also view nightly generated code documentation (Available from the Developer Center), which includes all of Echo's classes.

Getting Started

First of all, you have to create your AI files. In glob2/src, you can add AI*.h and AI*.cpp, where * is whatever your AI is called. Next, you need to add copyright statements to your files. Find any other source file, and copy&paste the copyright statements from the top of it to the top of your AI*.h and AI*.cpp files, replacing the name on the copyright statements with your name. Next, open up Makefile.am in glob2/src. You'll notice a list of source files, add your AI*.h and AI*.cpp to their correct position alphabetically. Add in header guards to your header file, and your ready to move on.

Now, we need to copy your AI structure over. Open up AIEcho.h, scroll about 3/4 of the way down. Your looking for a class called "EchoAI". When you find it, copy it over to your AI*.h. Remove all of the virtuals and =0's from the functions, and replace EchoAI with the name of your AI. Publicly derive your class from AIEcho::EchoAI, and add #include "AIEcho.h" to the top of your file. Now, implement all of the functions in your AI in the source file, as simple, empty functions (you won't be adding code yet).

Finally, we need to make your AI recognized by Glob2. You can use ReachToInfinity as an example Echo AI here. Open up AI.h and AI.cpp. At the top of AI.h, you'll find an enum with all of the AI names in uppercase. Add yours after all of the AI's, but before the "SIZE" variable. In AI.cpp, add the #include "AI*.h" at the top for your AI. Now, you're going to change the AI constructor. You'll see a switch statement at the top of the constructor (which should be at the top of the file). Add in a case statement for your AI. Then, set up a your initializer like the following:

 aiImplementation=new AIEcho::Echo(new your_ai_name, player);

Remember to replace your_ai_name with the name of you're actual AI. Now, scroll down the file further until you see another switch statement in AI::load. Add you're AI to this one as well, with the same initializer as above. Except, this time, after you're initializer, add the following:

 aiImplementation->load(stream, player, versionMinor);

We have one last thing to do, and thats add you're AI name to the translations. in glob2/data, you will see a bunch of texts.*.txt files for all of the translations. Open up texts.keys.txt, and scroll down to where the AI's are (you can search for [AI] in your text editor to make this faster). Add your AI name as a key, however, be very carefull to put the key in the exact position that you're AI is in the enum in AI.h. As of now (July 10'th 2006), [AIToubid] is an experiemental AI, so you have to put you're AI's key before his (since his was in the expiremental section of the enum in AI.h). Now, for the tedious bit, you have to add you're AI to each of the translations. You will have to open them up, one by one, and add you're AI name to the same position you added it in texts.keys.txt. Not only do you add your AI name in brackets, you also add it in text. Ignore the fact that you can't translate you're own AI name to every language, its important that atleast the key exists, so when translators do come along, they can translate it.

That was allot of work just to make a new AI, however, you'll find its well worth your time. You can compile everything, load the game up, and see your now-empty AI do its work. Its time to start implementing your AI! Its reccomended you add "using namespace " to the top of your source file for all of the namespaces located in AIEcho.h (they are conviently prototyped at the top of the file).

Constructing Buildings and Flags

How to

For starters, you need to make a BuildingOrder. This is an order that will be passed to Echo once you have provided all of the information to construct a building. A BuildingOrder takes three things in its constructor, a pointer to the player your team is on (obtained from echo.player), the building type (available from IntBuildingType.h, remember to do IntBuildingType::SWARM_BUILDING, instead of just SWARM_BUILDING, and the same for other buildings), and the number of workers that will be used to construct the building. After you've created the building order, you now need to add constraints to the order to choose where the building will be located.

Constraints are added via BuildingOrder::add_constraint. There are five constraints to choose from:

  • MaximumDistance - can't be to far from a provided object. Takes a GradientInfo and an integer for maximum distance.
  • MinimumDistance - can't be to close to a provided object. Takes a GradientInfo and an integer for minimum distance.
  • MaximizedDistance - preferably farther than a provided object, but can be close. Takes a GradientInfo and an integer for weight, affecting how much this effect this constraint has on the final result.
  • MinimizedDistance - preferably closer to a provided object, but can be far. Takes a GradientInfo and an integer for weight, affecting how much this effect this constraint has on the final result.
  • CenteredOn - A special constraint that only allows the building/flag to be centered on another, provided building. Usefull for flags, to center them on an enemy. Will be discussed later.

To use a constraint, however, there is something you must do first. You must set the gradient information for that constraint. Gradient information is set with the GradientInfo class. Gradients are Echo's way of computing the distance to various objects. With a GradientInfo class at hand, you add sources and obstacles for the gradient. A source is something that distance will be counted from. An obstacle is an object that may get in the way of the distance compuation. For example, you want you're building to be placed close to other buildings. However, you don't want direct distance to be counted, because that distance could be going straight through ressources. Instead, you want it to count distance arround the ressources, in which case, you would add an obstacle for the ressources. Here is a list of entities that can count as sources and obstacles:

  • Building - A specifc type of building from a particular team. Takes a building type, a team number, and a boolean for whether the building is allowed to be under construction or not.
  • AnyTeamBuilding - any building from a particular team. Takes a team number, and a boolean for whether the building is allowed to be under construction or not.
  • AnyBuilding - Any building from any team. Takes a boolean for whether the building is allowed to be under construction or not.
  • Ressource - Any ressource of a particular type. Takes a ressource type. Those are listed in Ressources.h
  • AnyRessource - Any ressource. Takes no arguments.
  • Water - Water. Takes no arguments.

You can obtain your team number from echo.player->team->teamNumber. You can use the functions GradientInfo::add_source and GradientInfo::add_obstacle to add sources and obstacles to your gradient information.

When you're finally ready to add your order to echo, use Echo::add_building_order. This function returns an ID that you can use to add ManagementOrders to the function (described later in Managing Buildings). If, however, Echo can't find a place to put your building, this function will return INVALID_BUILDING.

Annotated Example

 //The main order for the race track, to be constructed using 6 workers.
 BuildingOrder bo(echo.player, IntBuildingType::WALKSPEED_BUILDING, 6);
 
//Constraints arround the location of wood GradientInfo gi_wood; //Add wood as a source for the gradient gi_wood.add_source(new Entities::Ressource(WOOD)); //You want to be close to wood, so use a MinimizedDistance. bo.add_constraint(new MinimizedDistance(gi_wood, 4));
//Constraints arround the location of stone GradientInfo gi_stone; //Add stone as a source for the gradient gi_stone.add_source(new Entities::Ressource(STONE)); //You want to be close to stone, so use a MinimizedDistance. However, the above gi_wood has a higher weight, so being close //to wood is more important bo.add_constraint(new MinimizedDistance(gi_stone, 1)); //You don't want to be to close, so you have room to upgrade. You use a MinimumDistance for this. bo.add_constraint(new MinimumDistance(gi_stone, 2));
//Constraints arround nearby settlement GradientInfo gi_building; //Add any building on my team as a source for the gradient. I don't want any construction sites counted, only completed buildings gi_building.add_source(new Entities::AnyTeamBuilding(echo.player->team->teamNumber, false)); //You don't want to calculate the distance through ressources, making the AI build on the other side of a big ressource wall. So, //Add AnyRessource as an obstacle gi_building.add_obstacle(new Entities::AnyRessource); //You want to be close to other buildings, but as the weight tells you, wheat is more important bo.add_constraint(new MinimizedDistance(gi_building, 2));
//You don't want to be too close to other buildings GradientInfo gi_building_construction; //This time, unlike the above, similar constraint, we want to allow construction sites to be counted gi_building_construction.add_source(new Entities::AnyTeamBuilding(echo.player->team->teamNumber, true)); //Again, we don't want to compute distance through ressources. gi_building_construction.add_obstacle(new Entities::AnyRessource); //Don't build the racetrack too close to other buildings or construction sites, a minimum distance of 4 means that there are going //to be three blocks in between them. A distance of one would cause the buildings to be right next to eachother. bo.add_constraint(new MinimumDistance(gi_building_construction, 4));
//Add the building order to the list of orders echo.add_building_order(bo);

Flags

Flags are created in the same manner as other buildings. However, since flags can be placed on top of other buildings, its sometimes usefull have a MaximumDistance constraint set to 0, causing the flag to be put directly on top of another object. This would not be valid for other buildings.

Summary

While constructing a building can be a lengthy ammount of code, you will be happy that your building is placed in a very intelligent location, and that its construction is managed for you. There are other things you can do after the code to order the construction of a building, such as add management orders, which will be explained in another section. This becomes very convienent and usefull.

Managing Buildings

How to

Managing buildings is a somewhat simpler task than constructing them. To manage a building, you first need to create a ManagementOrder. ManagementOrders come in a variety of types, some of them change the number of units assigned to a building, others change the size of a flag. Here is a list of them:

  • AssignWorkers - Takes the number of workers to assign, and it assigns it to the building
  • ChangeSwarm - Takes three ratios, and changes the ratios of worker, explorer, and warrior on a swarm
  • DestroyBuilding - Destroys a building
  • AddRessourceTracker - Attaches a ressource tracker to the building. Ressource trackers are explained later.
  • PauseRessourceTracker - Pauses a buildings ressource tracker. Usefull during upgrades.
  • UnPauseRessourceTracker - Unpauses a buildings ressource tracker. Usefull after an upgrade.
  • ChangeFlagSize - Changes the radius of a flag to the provided radius. Takes an integer for the size.
  • ChangeFlagMinimumLevel - Changes the minimum level that a unit must be to come to a flag. Takes an integer for the minimum level.

What makes ManagementOrders so usefull is the fact that they can have **Conditions** attached to them. A condition is attached using ManagementOrder::add_condition. The order will not be executed untill the condition has been satisfied. A prime example of this would be to change the number of units assigned after a building has finished construction. For this, you would attach a NotUnderConstruction condition to the AssignWorkers management order and send it right after you order construction of the building. When the building is done being constructed, its number of units will be changed automatically.

Keep in mind that Conditions are re-used in other parts of Echo, so some of the conditions may make little sense to attach to an order, even though its possible. Here is a list of Conditions:

  • NotUnderConstruction - Passes buildings that are not under any sort of construction
  • UnderConstruction - Passes buildings that *are* under construction
  • BeingUpgraded - Passes buildings that are being upgraded.
  • BeingUpgradedTo - Passes buildings being upgraded to a particular level. Takes an integer of the level.
  • SpecificBuildingType - Passes buildings buildings of a particular type. Takes an integer for the type.
  • NotSpecificBuildingType - Passes any buildings *but* the particular type. Takes an integer for the type.
  • BuildingLevel - Passes any buildings of the particular level. Takes an integer for the level.
  • Upgradable - Passes any buildings that can be upgraded. Eg, swarms can't be upgraded, level three buildings can't be upgraded, damaged buildings can't be upgraded, etc..
  • EnemyBuildingDestroyed - This is a special condition in that it doesn't reflect on the building its being examined with. This condition passes when a provided enemy building is destroyed. This takes a reference to Echo, and an integer gid refering to the enemy building (obtaining these GID's is explained in Obtaining Information). This is primarily usefull with warflags, to have your war flag automatically destroyed when the building its attack is destroyed.
  • TicksPassed - This is not a condition that is to be used under normal circumstances. It passes after a certain number of attempts. It is mainly used for debugging, you should not use this for real situations.

Any of these conditions can be attached to an Order, but only certain ones (such as NotUnderConstruction) make any sense to do so.

Once you have created your management order, and added any conditions that you desire, its finally time to pass it into Echo. You can use the function Echo::add_management_order to do this. add_management_order takes two arguments, the management order, and an integer specifying the building that management order to be called upon. The building is an integer ID, you can get it from the return of Echo::add_building_order for example. Other ways explained in Obtaining Information.

Some Examples

Here is an example where you want several things to be done after the construction of a swarm:

 //The main order for the swarm
 BuildingOrder bo(echo.player, IntBuildingType::SWARM_BUILDING, 3);
 //...
 //Add the building order to the list of orders
 unsigned int id=echo.add_building_order(bo);
 
 //Change the number of workers assigned when the building is finished
 ManagementOrder* mo_completion=new AssignWorkers(5);
 mo_completion->add_condition(new NotUnderConstruction);
 echo.add_management_order(mo_completion, id);
 
 //Change the ratio of the swarm when its finished
 ManagementOrder* mo_ratios=new ChangeSwarm(15, 1, 0);
 mo_ratios->add_condition(new NotUnderConstruction);
 echo.add_management_order(mo_ratios, id);
 
 //Add a tracker, when the swarm is finished
 ManagementOrder* mo_tracker=new AddRessourceTracker;
 mo_tracker->add_condition(new NotUnderConstruction);
 echo.add_management_order(mo_tracker, id);

Here is an example where we have are creating an exploration flag on an enemy building, and, if that building happens to be destroyed, we want our flag to be destroyed with it.

 unsigned int enemy_building_id=*ebi;
 BuildingOrder bo(echo.player, IntBuildingType::EXPLORATION_FLAG, 1);
 bo.add_constraint(new CenteredOn(enemy_building_id));
 unsigned int id=echo.add_building_order(bo);
 
 if(id!=INVALID_BUILDING)
 {
     ManagementOrder* mo_completion=new ChangeFlagSize(12);
     echo.add_management_order(mo_completion, id);
     
     ManagementOrder* mo_destroyed=new DestroyBuilding;
     mo_destroyed->add_condition(new EnemyBuildingDestroyed(echo, enemy_building_id));
     echo.add_management_order(mo_destroyed, id);
 }

Summary

ManagementOrders are very important, and the ability to attach a condition to them makes them much more convienent and usefull.

Areas and Alliances

How to

There is another type of ManagementOrder we have not yet discussed. It is called the GlobalManagementOrder. GlobalManagementOrders are different in that they don't affect one building in particular. They are more global in usage. GlobalManagementOrders can't have Conditions attached to them, mainly because Conditions are specific to buildings. This may be changed in the future. Here is a list of the three global management orders:

  • AddArea - Adds restricted area, guard area, or clearing area to the map.
  • RemoveArea - Removes restricted area, guard area, or clearing area from the map.
  • ChangeAlliances - Changes alliances with another player.

GlobalManagementOrders are made using a similar process to standard ManagementOrders, except that, to add them, you use Echo::add_global_management_order.

Add/Remove Area

These orders take one argument to their constructor, which is the type of area that you will be adding or removing. This is stored in an enum, available options are:

  • ClearingArea
  • ForbiddenArea
  • GuardArea

Once you create on of these, you must add the positions you wish to be changed, using add_location function that both of them have. add_location takes the x and y cordinates of the position you are specifying. Obtaining these cordinates can be done using functions from the Obtaining Information section.

ChangeAlliances

The ChangeAlliances order changes your alliances with an opponent. It can be done all in one line, unlike the other two orders. To its constructor, it takes:

  • An integer specifying the team your changing
  • A boost::logic::tribool specifying whether you should be allied with that team or not. Passing in boost::logic::indeterminate (as opposed to true or false) here will cause the value to keep whatever value it previoussly had, and not be changed. This rule is the same for the other arguments.
  • A boost::logic::tribool specifying whether you should be enemied with that team or not
  • A boost::logic::tribool specifying whether that team should be able to see your markets.
  • A boost::logic::tribool specifying whether that team should be able to see your inns. With this, you can convert your opponents units.
  • A boost::logic::tribool specifying whether that team should be able to see all of your units and buildings. This is generally for allied teams.

Some Examples

Here is a long example where Add and Remove area are used to farm wheat that is near water:

 //An order of areas to be added
 AddArea* mo_farming=new AddArea(ForbiddenArea);
 //An order of areas to be removed
 RemoveArea* mo_non_farming=new RemoveArea(ForbiddenArea);
 //Get the gradient that will be used to determine if a particular spot is close to water
 AIEcho::Gradients::GradientInfo gi_water;
 gi_water.add_source(new Entities::Water);
 Gradient& gradient=echo.get_gradient_manager().get_gradient(gi_water);
 
 for(int x=0; x<echo.player->map->getW(); ++x)
 {
     for(int y=0; y<echo.player->map->getH(); ++y)
     {
         //Qualify only positions that form a grid like pattern
         if((x%2==1 && y%2==1))
         {
             //Remove forbidden areas that no longer have a ressource in them.
             //Note that the reference to map will be replaced by functions built into Echo
             //At a later date
             if((!echo.player->map->isRessourceTakeable(x, y, WOOD) &&
                 !echo.player->map->isRessourceTakeable(x, y, CORN)) &&
                 echo.player->map->isForbidden(x, y, echo.player->team->me))
             {
                 mo_non_farming->add_location(x, y);
             }
             else
             {
                 //Add forbidden areas to ressources not already farmed.
                 //Only farm wood or wheat, and only farm ressources that we can see,
                 //and only ressources that are close to water.
                 //Again, note the reference to map will be removed at a later date
                 if((echo.player->map->isRessourceTakeable(x, y, WOOD) ||
                     echo.player->map->isRessourceTakeable(x, y, CORN)) &&
                     echo.player->map->isMapDiscovered(x, y, echo.player->team->me) &&
                     !echo.player->map->isForbidden(x, y, echo.player->team->me) &&
                     gradient.get_height(x, y)<10)
                 {
                     mo_farming->add_location(x, y);
                 }
             }
         }
     }
 }
 //Send the orders to echo
 echo.add_global_management_order(mo_farming);
 echo.add_global_management_order(mo_non_farming);


Heres an example where ChangeAlliance is used to start showing our inns to all of the enemy teams. The indeterminate is used here because we don't want to change anything except the trait assocciatted with inn view. enemy_team_iterator is discussed in the section Obtaining Information

 for(enemy_team_iterator i(echo); i!=enemy_team_iterator(); ++i)
 {
     GlobalManagementOrder* mo_alliance=new ChangeAlliances(*i, indeterminate, indeterminate, indeterminate, true, indeterminate);
     echo.add_global_management_order(mo_alliance);
 }

Summary

GlobalManagementOrders, unlike ManagementOrders, don't affect any particular building. They are global in a sense, but just as usefull as the other management orders. They are passed onto Echo via add_global_management_order.

Upgrading And Repairing Buildings

How to

Upgrading and repairing buildings is a very quick and painless proccess. First of all, to start the construction, you need to make an UpgradeRepairOrder. This order takes, in its constructor, a reference to Echo, an ID reffering to the building you want upgraded, and the number of workers you want spent upgrading the building. After you construct an instance of this, simply pass it on to Echo via Echo::add_upgrade_repair_order. After which, you can add ManagementOrders as you please. Rememeber, though, Echo will automatically set the number of workers assigned to the value you had it when you started construction. You can still add a ManagementOrder to change the number of units assigned once the building is finished being upgraded, it will make no difference.

Example

Here is an example where a random building is being upgraded. If that building is an Inn, then it will change the number of units assigned, as higher level inns need more workers. It will also Pause and UnPause ressource trackers on the Inn. It makes use of BuildingSearch, which is explained in the section Obtaining Information. It also makes use of a variable called school_counts, which was previoussly declared to be the number of schools.

 //Find all of our upgradable level one buildings.
 BuildingSearch bs(echo);
 bs.add_condition(new Upgradable);
 bs.add_condition(new BuildingLevel(1));
 //If we have only one school, we don't want to try to upgrade it
 if(school_counts<2)
     bs.add_condition(new NotSpecificBuildingType(IntBuildingType::SCIENCE_BUILDING));
 std::vector<int> buildings;
 std::copy(bs.begin(), bs.end(), std::back_insert_iterator<std::vector<int> >(buildings));
 
 if(buildings.size()!=0)
 {
     //Choose a random building using syncRand
     int chosen=syncRand()%buildings.size();
     UpgradeRepairOrder* uro = new UpgradeRepairOrder(echo, buildings[chosen], 8);
     echo.add_upgrade_repair_order(uro);
     
     //Some special rules surround Inns
     if(echo.get_building_register().get_type(buildings[chosen])==IntBuildingType::FOOD_BUILDING)
     {
         //Pause the ressource tracker once construction begins
         ManagementOrder* mo_tracker_pause=new PauseRessourceTracker;
         mo_tracker_pause->add_condition(new UnderConstruction);
         echo.add_management_order(mo_tracker_pause, buildings[chosen]);
         
         //Unpause the ressource tracker when construction ends
         ManagementOrder* mo_tracker_unpause=new UnPauseRessourceTracker;
         mo_tracker_unpause->add_condition(new NotUnderConstruction);
         echo.add_management_order(mo_tracker_unpause, buildings[chosen]);
         
         //When the upgade is finished, change the number of units assigned to 3
         ManagementOrder* mo_completion=new AssignWorkers(3);
         mo_completion->add_condition(new NotUnderConstruction);
         echo.add_management_order(mo_completion, buildings[chosen]);
     }
 }

Summary

You should find that using UpgradeRepairOrders is a fairly quick and simple proccess.

Misc