Making An AI With Echo (part 2)From Globulation2Making An AI With Echo (part 2)
ContentsAreas, Alliances, and Internal MessagingHow toThere 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).
Add/Remove AreaThese 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:
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. ChangeAlliancesThe 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:
Internal MessagingThe 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.
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 ExamplesHere 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); SummaryThese 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 BuildingsHow toUpgrading 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. ExampleHere 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)); SummaryYou should find that Upgrading and Repairing buildings using the UpgradeRepair order to be very easy and flexible. Obtaining InformationObtaining 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 BuildingsThis 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 NumbersGetting 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 BuildingsGetting 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); Getting Overall Statistics from the Glob2 engineIf 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:
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 BuildingsWe 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); 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); 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 LevelsHere 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); Getting Information About The MapIf 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:
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 SavingLoading 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. SavingThe 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(); LoadingLoading 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();
SummaryLoading 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).
SummaryEcho is still in development, but I'm sure you'll find the proccess of making an AI much easier and more flexible using it. | ||