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 04:32, 3 February 2007 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 your 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 two things in its constructor, 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 six 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.
  • CenteredOfBuilding - 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.
  • SinglePosition - A special constraint that only allows the building/flag to be centered on the provided x and y cordinates that it takes in its constructor.

To use one of the first 4 constraints, 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.

Be carefull, however. If you use an Source that does not exist on the map anywhere (for example, fruits don't exist on some maps), then the Gradient will fail at every point. If you create an Inn that is positioned relative to Fruit tress andf there are no fruit trees, the Inn will never be created.

There is another way you can create a GradientInfo, it is somewhat more convenient. Its called make_gradient_info, and make_gradient_info_obstacle. These two convenience functions take Entities as their arguments and return a GradientInfo using them. make_gradient_info can take one or two entities, both refering to sources. make_gradint_info_obstacle can take one or two sources, and one obstacle.

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

Heres an example of ordering the cosntruction of a racetrack, with some very specific requirements on the position.

 //The main order for the race track, to be constructed using 6 workers.
 BuildingOrder bo(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);

Here is the same code, except using make_gradient_info.

 //The main order for the race track, to be constructed using 6 workers.
 BuildingOrder bo(IntBuildingType::WALKSPEED_BUILDING, 6);
 
//Constraints arround the location of wood //You want to be close to wood, so use a MinimizedDistance. bo.add_constraint(new MinimizedDistance(make_gradient_info(new Entities::Ressource(WOOD)), 4));
//Constraints arround the location of 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(make_gradient_info(new Entities::Ressource(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(make_gradient_info(new Entities::Ressource(STONE)), 2));
//Constraints arround nearby settlement //You want to be close to other buildings, but as the weight tells you, wheat is more important bo.add_constraint(new MinimizedDistance(make_gradient_info_obstacle( new Entities::AnyTeamBuilding(echo.player->team->teamNumber, false), new Entities::AnyRessource), 2));
//You don't want to be too close to other buildings //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(make_gradient_info_obstacle( new Entities::AnyTeamBuilding(echo.player->team->teamNumber, true)), new Entities::AnyRessource), 4));
//Add the building order to the list of orders echo.add_building_order(bo);

Its shorter, but it can also cause for some fairly long lines. You can use which-ever one you prefer.

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.

Conditional Construction

There are ways for Buildings to be automatically constructed when certain condtions are passed. This is called the condition system. Conditions are described in further detail in the Managing Buildings section. To use conditions with a BuildingOrder, simply use the function BuildingOrder::add_condition in the same manner as you would for a ManagementOrder.

This can be used, for example, in chained construction, meaning after one building finishes being constructed, the next building is instantly started. This kind of a system could also be done using the Internal Messaging system. How you do things is up to you.


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 the next 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. The management orders that operate on a building (all but the last 4) take the buildings id number as their last argument. 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.
  • UpgradeRepair - Orders your building to be upgraded, or repaired if its damaged (to be discussed later)
  • AddArea - Adds restricted area, guard area, or clearing area to the map (to be discussed later)
  • RemoveArea - Removes restricted area, guard area, or clearing area from the map (to be discussed later)
  • ChangeAlliances - Changes alliances with another player (to be discussed later)
  • SendMessage - Sends a message to your AI's internal messaging system (to be discussed later)

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, the NotUnderConstruction condition will match, and then the AssignWorkers order will follow through.

  • ParticularBuilding - This condition bassically wraps the building conditions listed below. This will be detailed further below.
  • BuildingDestroyed - This condition matches when the provided building is destroyed. It takes the ID of the building.
  • EnemyBuildingDestroyed - This condition is matached when the provided enemy building is destroyed. It takes a reference to the Echo System for technical reasons, and the gbid of the enemy building.
  • EitherCondition - Takes two conditions, and mataches if one or the other matches. This can be used for condition logic.
  • AllConditions - Takes 1 to 4 conditions (Conditions that are NULL are ignored) and matches when all of them match.
  • Population - Matches when a population condition is reached. It takes 3 booleans designating whether workers, explorers, and warriors respectivly are to be counted, and integer that designates the number that has to be matched, and a PopulationMethod. The two PopulationMethods are Population::Greater and Population::Lesser, which causes the condition to match when the population exceeds or goes below the given amount respectivly.

There are numerous conditions that operate on buildings. These conditions are used to both count and find buildings that match these conditions (explained later), or in this case, they can match when the properties of one particular building meet the requirements. To use them like you would the above conditions, you must wrap them in ParticularBuilding and provide the ID of the building that is to be tested. ParticularBuilding takes the BuildingCondition as its first argument, and the ID of the building to be tested as its second. The building ID is an integer ID, you can get it from the return of Echo::add_building_order for example. Other ways of getting an ID are explained in Obtaining Information.

  • 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..
  • RessourceTrackerAmount - Passes any buildings whos ressource tracker matches the provided amount. It takes an integer providing the amount as its first argument, and a TrackerMethod as its second. The TrackerMethod is one of RessourceTrackerAmount::Greater or RessourcetrackerAmmount::Lesser. Be carefull not to use this on buildings that aren't garunteed to have a ressource tracker already, as this will not create a ressource tracker and will result in a crash.
  • RessourceTrackerAge - Passes any buildings whos ressource tracker's age matches the provided age. It takes an integer for the age as its first argument, and a TrackerMethod as its second. The TrackerMethod is one of RessourceTrackerAge::Greater or RessourceTrackerAge::Lesser. Like the above, be carefull not to use this on buildings that have no ressource tracker.

Any of these conditions can be attached to an Order, but only certain ones (such as NotUnderConstruction) make any sense to do so, because their values can change. Other ones, Like SpecificBuildingType, are more for sorting through buildings as explained later, since a building's type doesn't change during its existance/

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 only one argument, the management order.

Some Examples

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

 BuildingOrder* bo = new BuildingOrder(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, id); //This adds the condition that the building must be finished construction before the workers will be reassigned. mo_completion->add_condition(new ParticularBuilding(new NotUnderConstruction, id)); //Add the management order to the queue. echo.add_management_order(mo_completion);
//Change the ratio of the swarm when its finished ManagementOrder* mo_ratios=new ChangeSwarm(15, 1, 0, id); //Again, the building must be completed before its ratios are changes mo_ratios->add_condition(new ParticularBuilding(new NotUnderConstruction, id)); //Add the management order to the queue echo.add_management_order(mo_ratios);
//Add a ressource tracker to track the amount of wheat in the swarm/ ManagementOrder* mo_tracker=new AddRessourceTracker(12, id, CORN); //We don't want to track ressource ammounts during construction, only after construction mo_tracker->add_condition(new ParticularBuilding(new NotUnderConstruction, id)); //Add the management order to the queue echo.add_management_order(mo_tracker);


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 = new BuildingOrder(IntBuildingType::EXPLORATION_FLAG, 1);
 bo->add_constraint(new CenterOfBuilding(enemy_building_id));
 unsigned int id=echo.add_building_order(bo);
 
if(id!=INVALID_BUILDING) { ManagementOrder* mo_completion=new ChangeFlagSize(12, id); echo.add_management_order(mo_completion);
ManagementOrder* mo_destroyed=new DestroyBuilding; mo_destroyed->add_condition(new EnemyBuildingDestroyed(echo, enemy_building_id)); echo.add_management_order(mo_destroyed); }

Summary

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

Areas, Alliances, and Internal Messaging

How to

There are four management orders that don't operate on any one particular building. Instead, they are more global. They operate just the same as normal ManagementOrders, they can have conditions related to a particular building (this even comes in usefull at some points).

  • 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.
  • SendMessage - Sends a message to your AI's internal messaging system

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 in the Management namespace, 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 a variety of methods. The Obtaining Information section explains this in more detail.

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.

Internal Messaging

The internal messaging system is a very useful system. It allows your AI to respond to certain Conditions passing. This is why you have a handle_message function. Its simple, flexible, and powerfull.

  • You create a SendMessage Management Order. It takes a string as its only argument, denoting the message.
  • When the order goes through, the handle_message function will be called with the message you provided. At this point, your AI can perform some pre-designated action.

A good usage of this system would be to recreate buildings that have been destroyed. You would create a SendMessage ManagementOrder, adding the BuildingDestroyed condition to it. Then, if the building is destroyed, the SendMessage will go through, sending the message that the building was destroyed so your AI can order its reconstruction. This is demonstrated in the examples.

Some Examples

Here is an example where AddArea and RemoveArea are used to farm wheat that is near water. It uses information obtained using the tools described in the ObtainingInformation section.

 AddArea* mo_farming=new AddArea(ForbiddenArea);
 RemoveArea* mo_non_farming=new RemoveArea(ForbiddenArea);
 AIEcho::Gradients::GradientInfo gi_water;
 gi_water.add_source(new Entities::Water);
 Gradient& gradient=echo.get_gradient_manager().get_gradient(gi_water);
 MapInfo mi(echo);
 for(int x=0; x<mi.get_width(); ++x)
 {
   for(int y=0; y<mi.get_height(); ++y)
   {
     if((x%2==1 && y%2==1))
     {
       if((!mi.is_ressource(x, y, WOOD) &&
           !mi.is_ressource(x, y, CORN)) &&
           mi.is_forbidden_area(x, y))
       {
         mo_non_farming->add_location(x, y);
       }
       else
       {
         if((mi.is_ressource(x, y, WOOD) ||
             mi.is_ressource(x, y, CORN)) &&
             mi.is_discovered(x, y) &&
             !mi.is_forbidden_area(x, y) &&
             gradient.get_height(x, y)<10)
         {
           mo_farming->add_location(x, y);
         }
       }
     }
   }
 }
 echo.add_management_order(mo_farming);
 echo.add_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)
 {
   ManagementOrder* mo_alliance=new ChangeAlliances(*i, indeterminate, indeterminate, indeterminate, true, indeterminate);
   echo.add_management_order(mo_alliance);
 }

Here is where the system automatically re-creates Inns that have been designated as destroyed. Some of the repeated code is removed.

 //The main order for the inn
 BuildingOrder* bo = new BuildingOrder(IntBuildingType::FOOD_BUILDING, 2);
 //..........
 //Add the building order to the list of orders
 unsigned int id=echo.add_building_order(bo);
 
ManagementOrder* mo_reconstruct = new SendMessage("construct inn"); mo_reconstruct->add_condition(new BuildingDestroyed(id)); echo.add_management_order(mo_reconstruct);
//........ In the handle_message function
if(message=="construct inn") { //The main order for the inn BuildingOrder* bo = new BuildingOrder(IntBuildingType::FOOD_BUILDING, 2); //........ //Add the building order to the list of orders unsigned int id=echo.add_building_order(bo);
//Reconstruct this in if its destroyed ManagementOrder* mo_reconstruct = new SendMessage("construct inn"); mo_reconstruct->add_condition(new BuildingDestroyed(id)); echo.add_management_order(mo_reconstruct); }

Summary

These Management Orders are unique in that they don't operate on Buildings, but instead on global values. But they are nevertheless easy to use and just as powerfull.

Upgrading And Repairing Buildings

How to

Upgrading and repairing buildings is a very simple process. To start an upgrade or a repair, you would use the UpgradeRepair management order, constructing it with the ID of the building you want to upgrade or repair. The system will upgrade the building if it is not damaged and can be upgraded (this can be tested with the Upgradable building condition). If the building is damaged, it will be sent into repair mode.

Example

Here is an example where a random building is being upgraded. It also has a few special rules in case the chosen building is an Inn. It will change the number of units assigned, as higher level inns need more workers and it will set up the ressource tracker to be Paused and UnPaused during the construction. It makes use of BuildingSearch, which is explained in the section Obtaining Information.

 //Find all of the buildings that can be upgraded
 BuildingSearch bs(echo);
 bs.add_condition(new Upgradable);
 bs.add_condition(new BuildingLevel(1));
 std::vector<int> buildings;
 //Copy the list of upgradable buildings into a vector
 std::copy(bs.begin(), bs.end(), std::back_insert_iterator<std::vector<int> >(buildings));
 
//If there are any buildings that can be upgraded if(buildings.size()!=0) { //Chose a building at random int chosen=syncRand()%buildings.size(); //Create the order for the building to be upgraded ManagementOrder* uro = new UpgradeRepair(buildings[chosen]); echo.add_management_order(uro);
//Find out the number of units assigned to the building, to be used later int assigned=echo.get_building_register().get_assigned(buildings[chosen]); //Assign 8 workers to perform the construction ManagementOrder* mo_assign=new AssignWorkers(8, buildings[chosen]); mo_assign->add_condition(new ParticularBuilding(new UnderConstruction, buildings[chosen])); echo.add_management_order(mo_assign);
//If this is an Inn, set the ressource tracker to be paused during construction, and assign 3 workers to the building after its done. if(echo.get_building_register().get_type(buildings[chosen])==IntBuildingType::FOOD_BUILDING) { //Pause the ressource tracker once construction has started ManagementOrder* mo_tracker_pause=new PauseRessourceTracker; mo_tracker_pause->add_condition(new ParticularBuilding(new UnderConstruction, buildings[chosen])); echo.add_management_order(mo_tracker_pause); //Unpause the ressource tracker when construction has finished ManagementOrder* mo_tracker_unpause=new UnPauseRessourceTracker; mo_tracker_unpause->add_condition(new ParticularBuilding(new NotUnderConstruction, buildings[chosen])); echo.add_management_order(mo_tracker_unpause); //Assign 3 workers to the building after the construction has finished. ManagementOrder* mo_completion=new AssignWorkers(3, buildings[chosen]); mo_completion->add_condition(new ParticularBuilding(new NotUnderConstruction, buildings[chosen])); echo.add_management_order(mo_completion); } else { //Since this building is not an Inn, set the number of units assigned to the building to its original amount after the construction is finished ManagementOrder* mo_assign=new AssignWorkers(assigned, buildings[chosen]); mo_assign->add_condition(new ParticularBuilding(new NotUnderConstruction, buildings[chosen])); echo.add_management_order(mo_assign); }

Summary

You should find that Upgrading and Repairing buildings using the UpgradeRepair order to be very easy and flexible.

Obtaining Information

Obtaining Information is a very important task to be able to do. There are a variety of ways you can obtain various pieces of information about your surrounding world. Many of Echo's information gathering functions are based upon iterators. There are a variety of iterators like this, such as the building_search_ierator and the enemy_team iterator. There are also the ressource trackers, another method of gathering information on your buildings. On top of them, there are the TeamStat objects, which is glob2's way of recording statistics,

Searching for Buildings

This system allows the programmer to search for buildings that match certain criteria. The conditions available where listed in the section "Managing Buildings". Lets say, for example, you want to list all of the level 1 Inns on your team. To do this, you first create a BuildingSearch object.

 BuildingSearch bs(echo);

Then, you can start adding conditions. You can attain a list of these from the "Managing Buildings" section. Note that you can only use the BuildingConditions, and you don't wrap them in ParticularBuilding, since these conditions will be used to test a large number of buildings.

 bs.add_condition(SpecificBuildingType(IntBuildingType::FOOD_BUILDING));
 bs.add_condition(BuildingLevel(1));

When your finished adding conditions, you can then count the buildings, or iterate through them. Counting the number of buildings is quick:

 unsigned int count=bs.count_buildings();

Iterating through them is a more usefull proccess. To iterate through them, you use building_search_iterator, returned by BuildingSearch::begin or BuildingSearch::end. Perhaps you wanted to iterate through your level 1 Inns, assigning 2 units to each of them.

 for(building_search_iterator i=bs.begin(); i!=bs.end(); ++i)
 {
     ManagementOrder* mo=new AssignWorkers(2, *i);
     echo.add_management_order(mo);
 }

The process for searching and counting buildings that satisfy conditions is as simple as that, but is very usefull.

Getting Enemy Team Numbers

Getting the team numbers of teams that are enemied with you is a usefull proccess in many areas, such as changing alliances. To start iterating, create an enemy_team_iterator. Instead of using an end() function however, you just make a default enemy_team_iterator and use it. The following code demonstrates using the enemy_team_iterator to go over all of the enemy teams and changing the alliance with them so that their units can see your Inns.

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

Getting Enemy Buildings

Getting the gbid's of enemy buildings is quite usefull in many circumstances. You only get so much information about your enemies buildings. You can iterate buildings you can see, and buildings that where in the game when the game started (the equivilent of a human looking at the map before a game). You can't iterate buildings that you can't see and where created after the game had started. To iterate through buildings, you create a enemy_building_iterator with five arguments, a reference to echo, the team number, the building type (or -1 for any), the building level, (or -1 for any), and a boost::tribool for whether is a construction site or not (or indeterminate for either). Here is an example where we use enemy_building_iterator and enemy_team_iterator to launch a large scale attack on all enemy swarms. The flags will be automatically destroyed after the swarm is destroyed.

 for(enemy_team_iterator i(echo); i!=enemy_team_iterator(); ++i)
 {
   for(enemy_building_iterator ebi(echo, *i, IntBuildingType::SWARM_BUILDING, -1, false); ebi!=enemy_building_iterator(); ++ebi)
   {
     BuildingOrder* bo = new BuildingOrder(IntBuildingType::WAR_FLAG, 15);
     bo->add_constraint(new CenterOfBuilding(*ebi));
     unsigned int id=echo.add_building_order(bo);
     
if(id!=INVALID_BUILDING) { ManagementOrder* mo_completion_size=new ChangeFlagSize(8, id); echo.add_management_order(mo_completion_size);
ManagementOrder* mo_completion_level=new ChangeFlagMinimumLevel(4, id); echo.add_management_order(mo_completion_level);
ManagementOrder* mo_destroyed=new DestroyBuilding; mo_destroyed->add_condition(new EnemyBuildingDestroyed(echo, *ebi)); echo.add_management_order(mo_destroyed); } } }

Getting Overall Statistics from the Glob2 engine

If you want just some general, quick statistics, you can use the TeamStat class. Have a look at it in TeamStat.h. If you want to get the most recent TeamStat, just call echo::get_team_stats. Team statistics are updated frequently. Here is a list of the statistics from TeamStat.h:

  • totalUnit - Total number of units
  • numberUnitPerType[type] - Number of units per type, possible types are WORKER, WARRIOR, and EXPLORER
  • totalFree - Total number of free units
  • isFree[type] - Free units per type
  • totalNeeded - Total units needed of runfilled jobs
  • totalBuilding - All finished buildings. Instead, use BuildingSearch.
  • numberBuilindPerType[type] - Finished buildings per type. Instead, use BuildingSearch.
  • numberBuildingPerTypePerLevel[type][level] - Finished buildings per type per level (6 levels, 0, 2, and 4 are construction sites, 1, 3, and 5 are completed). Use BuildingSearch instead here
  • needFoodCritical - The number of starving units
  • needFood - The number of hungry units
  • needHeal - The number of hurt units
  • needNothing - The number of Ok units
  • updgradeState[ability][level] - The number of units with a particular ability at a particular level. Look in UnitConsts.h for a list.
  • totalHP - The total HP of all your units
  • totalAttackPower - The total power of attacks from all your warriors. Calculated by warrior speed * warrior strength.
  • totalDefensePower - The combined attack power of all your towers
  • happiness - The number of units with a particular happiness level, IE the number of units that have 0 fruits, the number that have had 1 fruit, etc..

If there is an alternative in Echo itself, its reccomended you use Echo's functions instead of TeamStat. Otherwise, however, you can use these for some quick info.

Tracking Ressource Consumption in Buildings

We briefly mentioned earlier in the ManagementOrders section about ressource trackers, and how they could be attached to buildings. Ressource Trackers are just objects designed to track the consumption of ressources in a building. For example, they can track the ammount of wheat in an Inn or Swarm (one of the more usefull examples). To do this, first initiate an AddRessourceTracker order to a building, as in this example, where we order a ressource tracker after the construction of an Inn. AddRessourceTracker takes one argument, the length of the record, in ticks. A small number will get you recent results and fluctuate more frequently, where as a larger number will get you long term averages. There are 2.5 ticks per second.

 //The main order for the inn
 AIEcho::Construction::BuildingOrder bo(IntBuildingType::FOOD_BUILDING, 2);
 //...
 //Add the building order to the list of orders
 unsigned int id=echo.add_building_order(bo);
 
ManagementOrder* mo_tracker=new AddRessourceTracker(12, CORN, id); mo_tracker->add_condition(new ParticularBuilding(new NotUnderConstruction, id)); echo.add_management_order(mo_tracker);

As soon as the order is proccessed (when the Inn is finished construction), you can use the function Echo::get_ressource_tracker to get the ressource tracker. get_ressource_tracker returns a boost::shared_ptr<RessourceTracker>, which you must store the pointer in. You can use shared_ptr as if it where any other pointer. The class RessourceTracker has a couple of usefull functions. get_age tells you how long the ressource tracker has been operating (not including the times when it was paused), and get_total_level tells you the total ammount of ressources the building contained in the most recent recording period. Remember that the longer your recording period is, the higher get_total_level will be.

RessourceTrackers record information round robin based on the number of ticks you provide them. This means you always get the most recent information. In this example, we are iterating through our Inns using a building_search_iterator, and ordering the destruction of Inns that have been arround for a long time, but still have low ammounts of wheat, which may mean that they are no longer close to wheat, and are hard to upkeep. Because RessourceTracker returns long term results, its not easy for a small anomaly to cause the destruction of an Inn that doesn't need to be destroyed.

 BuildingSearch inns(echo);
 inns.add_condition(new SpecificBuildingType(IntBuildingType::FOOD_BUILDING));
 inns.add_condition(new NotUnderConstruction);
 for(building_search_iterator i=inns.begin(); i!=inns.end(); ++i)
 {
   boost::shared_ptr<RessourceTracker> rt=echo.get_ressource_tracker(*i);
   if(rt->get_age()>1500)
   {
     if(rt->get_total_level() < 24*echo.get_building_register().get_level(*i))
     {
       ManagementOrder* mo_destroy=new DestroyBuilding(*i);
       echo.add_management_order(mo_destroy);
     }
   }
 }

RessourceTrackers record things arround the clock. However, its sometimes usefull to temporarily pause a ressource tracker. The Managing Buildings section briefly wen't over this. To do this, you use the PauseRessourceTracker management order. This order takes no arguments. To un-pause a ressource tracker, you use the UnPauseRessourceTracker management order. This order also takes no arguments. When a ressource tracker is paused, neither its age or its records get changed. In this example, we have just ordered an Inn to be upgraded, so we temporarily want the ressource tracker to be paused during the construction.

 ManagementOrder* uro = new UpgradeRepair(buildings[chosen]);
 echo.add_management_order(uro);
 
ManagementOrder* mo_tracker_unpause=new UnPauseRessourceTracker; mo_tracker_unpause->add_condition(new ParticularBuilding(new NotUnderConstruction, buildings[chosen])); echo.add_management_order(mo_tracker_unpause);
ManagementOrder* mo_completion=new AssignWorkers(3, buildings[chosen]); mo_completion->add_condition(new ParticularBuilding(new NotUnderConstruction, buildings[chosen])); echo.add_management_order(mo_completion);

There are also building conditions assocciatted with ressource trackers. They are RessourceTrackerAmmount and RessourceTrackerAge. They are described in the section "ManagingBuildings".

You may find ressource trackers to be a usefull method for tracking consumption in a variety of buildings. Remember, though, if you try to access the ressource tracker on a building that doesn't have one, you will get a pointer of NULL, and will crash the program if you try to use it.

Getting Building Types and Levels

Here is a quick piece of information about getting the types of buildings and the levels of buildings you have the IDs for. First, you call echo.get_building_register() to get a BuildingRegister reference. You can then call get_level and get_type on the BuildingRegister to get the type and level, as in the following example. Here, we are iterating through our buildings and the start of the game, and setting the correct ratios, assigning the right number of units, and adding ressource trackers to the Swarms and Inns that we are starting with.

 BuildingSearch bs(echo);
 for(building_search_iterator i = bs.begin(); i!=bs.end(); ++i)
 {  
   if(echo.get_building_register().get_type(*i)==IntBuildingType::SWARM_BUILDING)
   {
     ManagementOrder* mo_completion=new AssignWorkers(5, *i);
     echo.add_management_order(mo_completion);
     
ManagementOrder* mo_ratios=new ChangeSwarm(15, 1, 0, *i); mo_ratios->add_condition(new ParticularBuilding(new NotUnderConstruction, *i)); echo.add_management_order(mo_ratios);
ManagementOrder* mo_tracker=new AddRessourceTracker(12, CORN, *i); echo.add_management_order(mo_tracker); } if(echo.get_building_register().get_type(*i)==IntBuildingType::FOOD_BUILDING) { ManagementOrder* mo_tracker=new AddRessourceTracker(12, CORN, *i); echo.add_management_order(mo_tracker); } }

Getting Information About The Map

If you want to get pieces of information about the map, you use the MapInfo class. The MapInfo class has a variety of functions you can use to get information about the map. Here they are, listed:

  • get_width - Gets the width of the map
  • get_height - Gets the height of the map
  • is_forbidden_area - Tells whether the position is forbidden area for your team
  • is_guard_area - Tells whether the position is guard area for your team
  • is_clearing_area - Tells whether the position is clearing area for your team
  • is_discovered - Tells whether the position is discovered by your team
  • is_ressource - Tells whether the position is a particular type of ressource, takes an integer specifying the type
  • is_water - Tells whether a position is water

Here is an example where we are counting the amount of water on the map:

 int amount=0;
 MapInfo mi(echo);
 for(int x=0; x<mi.get_width(); ++x)
     for(int y=0; y<mi.get_height(); ++y)
         if(mi.is_water(x, y))
             amount++;

MapInfo is a simple and direct class, taking a reference to Echo in its constructor. You should find it painless to use.

Loading and Saving

Loading and saving may seem like a tricky process. Indeed, at times it can be very time consuming, especially loading and saving containers. Your AI's have to overload two functions when loading and saving, EchoAI::load and EchoAI::save respectivly.

Saving

The first part of your save function should look like this:

 stream->writeEnterSection("your_ai_name");

Similairly, the last part of your save function should look like this:

 stream->writeLeaveSection();

Both of these functions are a requirement. Inbetween those two calls, you put in your saving code. You can save a variety of types on information, but generally, it comes down to integers. You use the function OutputStream::writeSint32 to write a 32 bit signed integer, for example (Sint32 is an SDL type, automatically typedefed to the 32 bit integer on the host system). Similairly, to write an unigned 8 bit integer, you would use the function OutputStream::writeUint8, where Uint8 is also an SDL typedef. Functions of this kind are provided for signed and unsigned 8, 16, and 32 bit integers. You can also write floats, using OutputStream::writeFloat and OutputStream::writeDouble. It is reccomended you do not use floats or doubles in your AI code, they are a nightmere when it comes to Net games, because different computers float implementations may produce different results for a particular calculation, causing two networked computers to become de-syncronized. Lastly, there is OutputStream::writeText, for any other kind of textual values.

All of those functions take two arguments, the value as the first argument, and a name as the second argument. The name is used in debugging, and is important even if you don't intend to use it. Its generally reccomended you keep the second name the same as your variable name. We observe this in the following code:

 stream->writeUint32(timer, "timer");
 stream->writeUint32(flag_on_cherry, "flag_on_cherry");
 stream->writeUint32(flag_on_orange, "flag_on_orange");
 stream->writeUint32(flag_on_prune, "flag_on_prune");

Writing containers is another tricky buisness. There are no special overloads for containers, but you should use the following idiom:

 stream->writeEnterSection("container_name");
 stream->writeUint32(container_name.size(), "size");
 Uint32 container_name_index=0;
 for(container::iterator i=container_name.begin(); i!=container_name.end(); ++i, ++container_name_index)
 {
     stream->writeEnterSection(container_name_index);
     //Write all of your values in the container here
     stream->writeLeaveSection();
 }
 stream->writeLeaveSection();

Loading

Loading is similair to saving in several respects. At the top of your load function, you should have this:

 stream->readEnterSection("your_ai_name");

And at the bottom, you should have this:

 stream->readLeaveSection();

Loading is also done with similar functions, such as InputStream::readUint32, and InputStream::readText. The read functions take one argument, the name of the value to be read. Again, this is used mainly for debugging, however, you should put these values in anyways even if you don't intend on debugging your code. These functions return the value being read. Again, containers can be tedious to read in, however, if you used the above idiom, you can follow this one as well:

 stream->readEnterSection("container_name");
 Uint32 container_name_size=stream->readUint32("size");
 for(Uint32 container_name_index=0; container_name_index<container_name_size; ++container_name_index)
 {
     stream->readEnterSection(container_name_index);
     //Load all of your information into the container.
     stream->readLeaveSection();
 }
 stream->readLeaveSection();


Summary

Loading and saving are both tedious tasks. Its important that you load and save values in the same order, and that you load and save *all* values that can't be recomputed when you load back up (an example of values that can be recomputed would be Echos gradients, which are not saved and loaded).


Summary

Echo is still in development, but I'm sure you'll find the proccess of making an AI much easier and more flexible using it.

Misc