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

Making An AI With Echo (part 2)

From Globulation2

Jump to: navigation, search

Back to Making An AI With Echo (part 1)


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 useful 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 coordinates of the position you are specifying. Obtaining these coordinates 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 previously 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 powerful.

  • 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);

Here's 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 associated 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 powerful.

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 resource 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_inserter(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_iterator and the enemy_team iterator. There are also the resource 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 useful process. 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 useful.

Getting Enemy Team Numbers

Getting the team numbers of teams that are enemied with you is a useful process 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 useful 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 equivalent 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, it's recommended you use Echo's functions instead of TeamStat. Otherwise, however, you can use these for some quick info.

Tracking Resource Consumption in Buildings

We briefly mentioned earlier in the ManagementOrders section about resource trackers, and how they could be attached to buildings. Resource Trackers are just objects designed to track the consumption of resources in a building. For example, they can track the amount of wheat in an Inn or Swarm (one of the more useful examples). To do this, first initiate an AddRessourceTracker order to a building, as in this example, where we order a resource 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 processed (when the Inn is finished construction), you can use the function Echo::get_ressource_tracker to get the resource 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 useful functions. get_age tells you how long the resource tracker has been operating (not including the times when it was paused), and get_total_level tells you the total amount of resources 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 around for a long time, but still have low amounts 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 around the clock. However, its sometimes useful to temporarily pause a resource tracker. The Managing Buildings section briefly went over this. To do this, you use the PauseRessourceTracker management order. This order takes no arguments. To un-pause a resource tracker, you use the UnPauseRessourceTracker management order. This order also takes no arguments. When a resource 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 resource 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 associated with resource trackers. They are RessourceTrackerAmmount and RessourceTrackerAge. They are described in the section "ManagingBuildings".

You may find resource trackers to be a useful method for tracking consumption in a variety of buildings. Remember, though, if you try to access the resource 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 resource 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 resource, 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 respectively.

Saving

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

 stream->writeEnterSection("your_ai_name");

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

 stream->writeLeaveSection();

Both of these functions are a requirement. In between 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). Similarly, to write an unsigned 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 recommended you do not use floats or doubles in your AI code, they are a nightmare 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-synchronized. 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 recommended 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 business. 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 similar 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 process of making an AI much easier and more flexible using it.