<-- -->

Free Web Hosting : Free Hosting : Troubled Teens : Report Abuse

Civ Evo Developer FAQ

Forum | Index

Notes | Contents by: Type | Command

AI Development

About This FAQ

The FAQ begins with simple answers where definite solutions are possible. Later topics are complex and involve uncertainty.

The FAQ assumes that you have read the AI Developer Manual and the Civ Evo in-game manual and reviewed the StdAI source code (StdAI.dpr in the Civ Evo source code) or another complete AI. Published source code is refered to eg AI Lib, StdAI (Standard AI). Where no source is specified for a file, it is from the Civ Evo source eg core.pas, term.pas. You can download the Civ Evo source code (including StdAI) from the Files section of the Civ Evo homepage.

Example code is written in Delphi with RO as a pointer (pTribe) to the current player's RO. Short examples assume an implicit "with RO^ do" block. For StdAI-based AIs convert this to MyRO and make loose functions into methods (of TStdAI). For TModularAI/TBasicAI-based code, change RO to cRO. If you're not familiar with Delphi it might seem odd that "with RO^ do" is used but "RO.Un[..." instead of "RO^.Un[...". This is just an oddity of Delphi - both versions have the same meaning.

How to make server command X work?

There are four general techniques to use a server command where you can't get it to work,

Each server command has a brief description in the AI Development Manual (which is in the AI Developer subdirectory of the Civ Evo installation). There is a list of documentation and example code.

You can ask for help in the Civ Evo forum or the TCEP forum.

Each server command indicates if it succeeded by it's result. So you can use trial-and-error at runtime by testing the result eg you could try each possible call until the result is successful. This is adequate if it doesn't take too long to find the right values.

If these three methods fail it could be worthwile to review the server source code. The Server function is implemented in Core.pas. Search for the command you are interested in and try to deduce the game rules from it's implementation.

How to find the territory of a tile? (Civ Evo 0.8, 20th March)

The fTerritory tile flag is a mask for the player which the tile belongs to (is the territory of). The territory field is in bits 27-30.

 TileTerritory := (RO.Map[Loc] and fTerritory) shr 27;

Unoccupied territory returns player 15 as the owner. You can check for no owner by testing fOwned.

if (RO.Map[Loc] and fOwned) <>0 then //a player owns this tile.
  TileTerritory := (RO.Map[Loc] and fTerritory) shr 27
else //no player owns this tile.
  ...

How to detect a new/captured unit or city or model?

When a new unit or city or model is made or you capture one, the Status property is initialised to 0.

How to detect when a unit is destroyed or captured?

During opponent's turns you can do this by analysing enemy moves.

A lost unit will have it's Loc property set to the special value -1.

It doesn't make sense to use lost units in server calls so you should test the Loc for a unit before using server calls.

During your turn you can destroy units by moving them or by disbanding them. Settlers and engineers can also be destroyed by founding/increasing cities (sFoundCity) although this can fail.

By coincidence, a method of CLocation (CUn is derived from CLocation - it inherits this method) can be used to test if a unit is lost. The Valid method tests if the Loc property is within the valid range for a map location. Because map locations may not be negative this will detect a value of -1 and return false.

So you could use

 if Un[counter].Valid() then ...

Instead of

 if Un[counter].Loc <> -1 then ...

Using Valid is slower because it also tests if Loc is not higher than the maximum map location.

You could use a constant instead to make the test readable.

 if Un[counter].Loc <> LostCode then ...

How to detect when a city is destroyed or captured?

During opponent's turns you may be able to do this by analysing enemy moves.

A lost city will have it's Loc property set to the special value -1.

It doesn't make sense to use lost cities in server calls so you should test the Loc for a city before using server calls.

You could lose a city during your turn by trading it with another player. (Trading cities is not implemented in Civ Evo 0.7.1).

By coincidence, a method of CLocation (CCity is derived from CLocation - it inherits this method) can be used to test if a city is lost. The Valid method tests if the Loc property is within the valid range for a map location. Because map locations may not be negative this will detect a value of -1 and return false.

So you could use

 if City[counter].Valid() then ...

Instead of

 if City[counter].Loc <> -1 then ...

Using Valid is slower because it also tests if Loc is not higher than the maximum map location.

You could use a constant instead to make the test readable.

 if City[counter].Loc <> LostCode then ...

How to associate short-term data with a unit or city or model?

The index of units in RO.Un[] and RO.EnemyUn[], cities in RO.City[] and RO.EnemyCity[], and models in RO.Model[] and RO.EnemyModel[] is constant during the current turn (between cTurn and sTurn). For data which is generated each turn (ie which doesn't need to be remembered from previous turns) you can make use of this.

You could store the index of the item (or a pointer to the item) with the data at the start of the turn before you generate it. Or you could have a list of references to data which is the same size as the number of items. The list is initialised at the start of the turn when you create the data. (For RO.EnemyUn[], RO.EnemyCity[] and RO.EnemyModel[] new items can be discovered during the turn.)

How to associate long-term data with a unit or city or model?

These types (TUn/CUn, TCity/CCity, TModel) each have a Status property. This is a special variable because you can save it by using the method SetStatus (or calling sSetUnitStatus, sSetCityStatus or sSetModelStatus). And it will be restored if your AI is loaded. You can also read and write it like a normal variable but it is only saved by the method or the server call.

For simple data such as flags you could use Status directly to store the flags.

For data which is too large to fit in Status you need to generate a reference to the data from Status.

You could store a reference or pointer in Status but if you need to save the data it could be hard to reload it at the same address.

To avoid this you can use another layer of indirection - have an array or list of pointers to each data item. If the data are classes you could add each instance to the list in it's constructor and remove it in it's destructor.

//Example where the data uses a class.
//This would work for a multiple player AI module.
//But a separate GameData list for each player would be easier to debug.

interface

uses Classes;   //TList

type
  TDataClass = class
    ... //data
    constructor Create;
    destructor Destroy; override;
  end;

var
  GameData :TList; //The list of data items

implementation

constructor TDataClass.Create;
begin
  inherited Create;
  GameData.add(Self); //add to the list of data items.
  ...             //initialise data
end;
destructor TDataClass.Destroy;
begin
  GameData[GameData.indexof(Self)] := nil; //notify data item freed
  ...             //free data
  inherited Destroy;
end;

initialization   //Create the list for the data items.
  GameData := TList.create;
  //Set index 0 to a nil pointer.
  GameData.add(nil^);	//useful for debugging (0 is the default value for Status).
finalization //you might want to make sure all the data items have been freed
  GameData.free;
end.

To associate data with a unit or city or model you set it's Status to the index of the data.

  //Create a data item
  MyData := TDataClass.Create;
  //Associate a unit with the data
  Un[uix].SetStatus(GameData.indexof(MyData));
  //Access the data for a particular unit (MyUnit is a TUn/CUn)
  TDataClass(GameData[MyUnit.Status])

//If you don't like GameData.indexof(MyData) you can add a method to TDataClass to return it's index
//which would allow:
//Un[uix].SetStatus(MyData.Index);

//Function to return the index in GameData[] of this data item.
function TDataClass.Index :integer;
begin
  result := GameData.indexof(Self);
end;

If you have more than one type of data you could implement them in separate classes. It would be neater to have a common base class to add or remove the data items from the list than to copy the constructor and destructor for each class.

type
  //The base class for data classes which can be associated with units or cities or models.
  TAssociatedData = class
    constructor Create;
    destructor Destroy; override;
    function Index :integer; //the index of this data item in GameData[].
  end;
  //Specific data classes
  TUnitData = class(TAssociatedData)
    ... //data
  end;
  TCityData = class(TAssociatedData)
    ... //data
  end;
  TModelData = class(TAssociatedData)
    ... //data
  end;

...

implementation

...

constructor TAssociatedData.Create;
begin
  inherited Create;
  GameData.add(Self); //add to the list of data items.
end;
destructor TAssociatedData.Destroy;
begin
  GameData[Index] := nil; //notify data item freed
  inherited Destroy;
end;
//Function to return the index of this data item in GameData[].
function TAssociatedData.Index :integer;
begin
  result := GameData.indexof(Self);
end;
To free your data items (eg in the finalization section) it is safe to call the Free method (TObject) on each item in GameData provided they are classes (all Delphi classes are ultimately derived from TObject). It would be logical to cast the data items to TAssociatedData but casting to TObject preserves flexibility eg you can add data items to GameData which are not associated with a specific game object.
procedure freeData;
var
  counter :integer;
begin
  for counter:=0 to GameData.count-1 do
    TObject(GameData[counter]).free;	//valid if only classes can be added.
  GameData.free;
end;
...
finalization
  freeData;
end.

For languages (eg C++) without an implicit common base class it would be simplest to cast to TAssociatedData although you could define an explicit base class with a virtual destructor for classes which the GameData list can use.

How to associate non-class data with a unit or city or model?

You can use a similar system as for class data.

You could just store the data in a class and use that.

If you choose not to use a class you can use the same methods as for class data.

interface

uses Classes;   //TList

type
  TData
    ... //data
  end;
  pData = ^TData;	//a pointer to a data item.

var
  DataList :TList; //The list of data items

implementation

...

initialization   //Create the list for the data items.
  DataList := TList.create;
  //Set index 0 to a nil pointer.
  DataList.add(nil^);	//useful for debugging (0 is the default value for Status).
finalization //you might want to make sure all the data items have been freed
  DataList.free;
end.

To create the data allocate memory for it and add a pointer to the data to DataList.

GetMem(MyData,sizeof(TData));	//MyData is a pData.
DataList.add(MyData);
//to free a data item (First get a pointer to it in MyData).
DataList.indexof(MyData) := nil;
FreeMem(MyData);

To associate it with a unit (or city or model), set Status to refer to it's index in DataList.

 Un[uix].SetStatus(DataList.indexof(MyData));

You can then access the data for a unit (or city or model) like this: (MyUnit is a TUn/CUn)

 pData(DataList[MyUnit.Status])^

You might want to make functions to create and associate and access a data item.

If you have class data as well you should be careful to use separate lists because you need different code to free classes and data.

If you could need to save the data it's desirable to use an explicit type so that you know the maximum size.

How to find the unit or city or model for my data?

The index in RO.Un[]/RO.City[]/RO.Model[] is constant during the current turn.

This means that by storing an index (or a pointer) with the data you can instantly find the unit or city or model. Note that units and cities can be lost during the current turn.

At other times (during enemy turns) you can work backwards from the reference the unit/city/model has to the data. Search through the list of units/cities/models for the item with the same reference.

How to design models?

First send an sCreateDevModel message with the domain for the model.

 CreateDevModel(TargetDomain);

Now use trial-and-error to set the model you want. (The range for each capability depends on which advances you possess.)

//Make an attacker (first set TargetAttack and TargetDefense)
while DevModel.Attack < TargetAttack do
  if not(IncCap(mcAttack)) then break;     //returns true if was able to increase the cap
while DevModel.Defense < TargetDefense do
  if not(IncCap(mcDefense)) then break;

You could set TargetAttack to be higher than the strongest enemy model's defense. For an attacker, TargetDefense could be 0.

The function IncCap is from AIAI by Anders Isaksson. It tries to increase a feature capability by one. If it succeeds it returns true.

function IncCap(Cap: integer): boolean;
begin
  Result := SetDevModelCap(Cap, RO.DevModel.Cap[Cap] + 1) = eOK;
end;

function DecCap(Cap: integer): boolean;
begin
  Result := SetDevModelCap(Cap, RO.DevModel.Cap[Cap] - 1) = eOK;
end;

When RO.DevModel is satisfactory, start researching it by calling sSetResearch with the special code, adMilitary.

if DevModel.Attack >= TargetAttack then
  SetResearch(adMilitary)
else
  //Design a different type of model or research an advance

If you couldn't order a model which met the target criteria, you could try to design a different model or you could research an advance instead. (Some advances improve the strength of models and the range of capabilities.)

How to find the model for an enemy unit?

This is not as simple as RO.EnemyModel[RO.EnemyUn[uix].mix]. You need to search RO.EnemyModel[] for a ModelInfo with the same Mix and Owner. There seems to be an error in 0.6.9 - sometimes there is no such ModelInfo!

//Function to find the model for an enemy unit, uix in RO.EnemyUn[]
//Based on ModelOfEnemyUn in term.pas
function ModelOfEnemyUn(uix :integer) :integer;
var
  counter :integer;
begin
  with RO^ do
  begin
    for counter := 0 to nEnemyModel-1 do
      if (EnemyModel[counter].owner = EnemyUn[uix].owner) and
         (EnemyModel[counter].mix = EnemyUn[uix].mix) then
      begin
        result := counter;
        exit;
      end;
    //Server error: There is no matching model!
    //Guess that it is the latest EnemyModel which belongs to that player
    for counter := nEnemyModel-1 downto 0 do
      if (EnemyModel[counter].owner = EnemyUn[uix].owner) then
      begin
        result := counter;
        exit;
      end;
    //Server error: There are no models for that player.
    //Guess that it is the latest enemymodel
    result := nEnemyModel-1;
  end;
end;

How to implement a repeatable random number generator?

You can make a random number generator repeatable by setting it's seed.

At the start of a game (on the initial turn) you should generate a seed randomly and save it.

To allow loading to an arbitrary turn you should initialise the random number generator seed at the start of each turn as a function of the game seed and the turn.

Randseed := GameSeed + RO.Turn; //simple additive function

Multiplayer AI modules should remember and recall the current player's (current) seed during diplomatic negotiation to avoid interaction between players controlled by the same AI module.

When you send an sc... client deactivation message save the current random number seed (it doesn't need to be saved to the save file only recorded in static data).

CurrentPlayer.SavedSeed := RandSeed;

When you receive an sc... client activation message (or cContinue) reset the seed from the saved seed.

RandSeed := CurrentPlayer.SavedSeed;

Alternatively you could use a separate random number generator for each player. This is necessary if players can execute concurrently.

How to intercept server calls?

You can intercept server calls by providing an auxilliary server function with the same type as TServerCall.

function AuxServer(Command, Player, Subject :integer; var Data) :integer; stdcall;

You should have an additional TServerCall variable eg ActualServer for the real Server.

When you set the Server variable (eg cInitModule) store it in ActualServer and initialise Server to AuxServer,

case Command of
  cInitModule:
  begin
    ActualServer := TServerCall(Data);
    Server := AuxServer;
...

All server calls (via Server) will be passed to AuxServer. AuxServer should pass on the call to the real server using ActualServer,

function AuxServer(Command, Player, Subject :integer; var Data) :integer;
begin
  ... // Before-call handling eg check for illegal calls.
  Result := ActualServer(Command, Player, Subject, Data);
  ... // After-call handling eg check for unexpected results.
end;

To explicitly avoid intercepting a call use ActualServer(...) instead of Server(...).

How to handle out-of-turn processing?

Out-of-turn processing can occur when you receive a cShowMove message. (Some of the other client messages are out-of-turn but you don't need to access RO or make server calls.)

Diplomatic negotiation is a sub-turn where you may use server calls.

Your RO is accessible but may not be fully up-to-date. Civ Evo 0.7 does not allow any server calls out-of-turn. (This suggests that you need to save the data from server information calls at the end of each turn if you might need to use it - but it might become out-of-date).

If you use a pointer or reference for the current AI (eg AIClasses AIme) you will need to update it. If your AI module is multiplayer you need to save the previous AI and restore it when your out-of-turn processing finishes.

You might need to save/restore the current random number seed.

The AI Lib version of AIClasses adds an initialisation routine to TCustomAI which sets AIme. TModularAI (ModularAI.pas) saves and restores the player state for show move messages in OnShowMove().

If you access RO as G.RO[Player] it isn't necessary to update references (but you can't use AIClasses).

How to decode the enemy move messages?

You need a handler for the cShowMove, cShowCapture, cShowAttackBegin, cShowAttackWon, cShowAttackLost messages.

You might need to save and restore the current state.

The handler can't make Server calls but it can access your RO. When you receive a cShowAttackBegin message you can decode the target location and search RO.Un[] for friendly units at that location. You could also search RO.EnemyUn[] if you are interested in attacks on other players.

with ShowMove do
  TargetLocation := CLocation(FromLoc).remote(dx,dy);
//Logical to check if observed but the map might not be updated?
if TargetLocation.TileFlags and fObserved <> 0 then
  ... //scan for RO.Un (and RO.EnemyUn) at the target location

Make a list of the units at the target location. If the attacker is successful you will receive a cShowAttackWon message. By comparing the units which remain with the list you can see which were destroyed.

with ShowMove do
  TargetLocation := CLocation(FromLoc).remote(dx,dy);
//Logical to check if all destroyed but the map might not be updated?
//The map isn't always updated for cShowCapture.
if TargetLocation.TileFlags and fUnit <> 0 then
begin
  ... //scan for RO.Un (or RO.EnemyUn) which remain at the target location
end
else
  ... //all the units were destroyed.

If you receive a cShowAttackLost message you can deduce that the attacker was destroyed and one of the defenders was damaged. (If you store the health of each unit in the list of units you can detect which defender was damaged and how much.) After the cShowAttack... (won or lost) message you should clear the list.

cShowCapture is sent when a city is captured (not necessarily yours). To detect it is your city you need to make a list of the locations of your cities at the end of each turn (and save it). When you receive cShowCapture you can test if it was your city by decoding the location and looking for it in the list. Or you could list cities which are not lost (loc <> -1) and check if any were lost when cShowCapture is received.

A cShowMove message consists of one step for an enemy unit. You need to link these up to see a path. In general you need to associate the path-data with the unit as you build it because units can be moved in any order.

How to decode enemy moves before the first turn?

You can decode cShowAttackWon, cShowAttackLost and cShowMove as normal.

In games with two players you can deduce that cShowCapture applies to one of your cities because you are the only opponent of your opponent. Otherwise you can only deduce that the city didn't belong to the player who captured it.

This could be a problem in scenarios.

How to handle diplomatic messages?

The messages are divided into client activation: (received like cTurn)
scContact, scDipStart, scDipNotice, scDipAccept, scDipCancelTreaty, scDipOffer, scDipBreak
and client deactivation: (sent like sTurn)
scContact, scReject, scDipStart, scDipNotice, scDipAccept, scDipCancelTreaty, scDipOffer, scDipBreak

Although there is overlap it's always clear from the context which is which (ie you send deactivations and receive activations).

procedure TStdAI.DoNegotiation;	//modified version
var
  Done : boolean;
begin
  Done := false;

It's vital to detect if you have not sent an appropriate client deactivation message - otherwise the server locks-up.

  inc(DipCount); //property

This is a crude technique to prevent an infinite loop - after a maximum number of messages break contact. It would be better to check if you have anything to sell and if the other player has anything you want.

  try

Catch any errors to guarantee that a client deactivation message will be sent.

    if (DipCount > 255) then DipCount := 0 else //trap to prevent infinite loop
    case ReceivedDipAction of

      scContact:
{Enemy asks for starting negotiation.
Possible deactivations: scDipStart, scReject}
      begin
        Done := DipAction(scDipStart)=eok;
        DipCount := 0;       //reset for new negotiation
      end;

      scDipStart:
{Enemy has accepted contact, start negotiation now.
Possible deactivations: scDipOffer, scDipCancelTreaty, scDipBreak}
      begin
        Done := MakeOffer;

MakeOffer() should send one of the possible deactivations and return true if the result is eok.

        DipCount := 0;       //reset for new negotiation
      end;

      scDipNotice:
{Enemy has noticed latest decision, continue with negotiation now.
Possible deactivations: scDipOffer, scDipCancelTreaty, scDipBreak}
        Done := MakeOffer;

      scDipAccept:
{Enemy has accepted latest offer, continue with negotiation now.
Possible deactivations: scDipOffer, scDipCancelTreaty, scDipBreak}
        Done := MakeOffer;

      scDipCancelTreaty:
{Enemy has canceled state treaty.
Possible deactivations: scDipNotice, scDipCancelTreaty, scDipBreak}
        Done := DipAction(scDipNotice)=eok;

      scDipOffer:
{Enemy makes offer.
Possible deactivations: scDipAccept (if made offer is allowed to be accepted),
 scDipOffer, scDipCancelTreaty, scDipBreak}
        Done := HandleOffer;

HandleOffer() should send one of the possible deactivations and return true if the result is eok.

      scDipBreak:
{Enemy breaks negotiation.
Possible deactivations: scDipNotice, scDipCancelTreaty}
        Done := DipAction(scDipNotice)=eok;
    end;
  except;  //swallow exceptions
  end;

  //guarantee deactivation
  if not(done) then
    if ReceivedDipAction <> scDipBreak then //can't reply scBreak to scBreak!
      DipAction(scDipBreak)
    else
      DipAction(scDipNotice);
end;

For debugging you might want to record exceptions in a log file. BasicAI.pas in AI Lib has example versions of MakeOffer() and HandleOffer().

How to start diplomatic negotiation?

You can start diplomatic negotiation during your turn by sending the client deactivation message scContact with the index of the player you want to negotiate with. After sending a client deactivation message you must exit the module (like sTurn).

You might need to save your random number generator seed.

When the diplomatic negotiation has finished you will receive a cContinue message and can resume your turn.

My pathfinder is too slow, how to speed it up?

There are several ways to make a pathfinder faster.

The most costly searches tend to be searches where the destination is not found because the search can wander all over the map trying every possible route.

To reduce the cost you should check if the search is legal before calling the pathfinder. For sGetMoveAdvice a destination tile which has unknown terrain or which requires a path through unknown terrain (eg is surrounded by unknown terrain) cannot be found by a search. Simple pathfinders can't cope with a destination which is the wrong domain ie ground units can only move to land tiles and sea units can only move to water tiles (or land tiles with canals).

If you could detect that the destination tile was the right domain but not reachable you could eliminate some impossible searchs. You could do this by maintaining a list of continents and oceans (see StdAI FormationInfo or AI Lib Formations.pas). But there are often large unknown areas which make it hard to conclude that two "continents" separated by an unknown region are truly separate.

//Function to test if a path search can succeed without calling the pathfinder.
//use as:
//... //setup pathfinder data eg target location
//if validTargetLoc(uix, TargetLocation) then  //uix=unit index in RO.Un[]
//  call pathfinder

function validTargetLoc(uix :integer; targloc :CLocation) :boolean;
begin
  with RO^ do
  begin
    result := false;

    //filter destinations which are beyond the poles
    if not(targloc.valid) then exit;
  
    //if your pathfinder can't use tiles with unknown terrain in a path
    if targloc.terrain = fUnknown then exit;
    //if the path finder can handle unknown terrain we need to check
    //if we know the terrain before we can check it's type.
    //It would be useful if we had a way to predict
    //the most likely terrain types for unknown tiles.
    if targloc.terrain <> fUnknown then
    begin

      //if your pathfinder can't use tiles with the wrong terrain for this unit's domain,
      if (Un[uix].common.domain < dSea) and
         (targloc.terrain <= fShore) then
        exit;	//ground/gadgetry unit trying to move to sea

      if (Un[uix].common.domain = dSea) and
         ( (targloc.terrain > fShore) and (targloc.tileflags and fCanal = 0)) then
        exit;	//sea unit trying to move to land (which doesn't have a canal).

      //You might want to test for legal but odd destinations also
      //eg air unit moving to tile without city or base (or enemy unit)
      //when it doesn't have any fuel left.

    end;

    //Passed all checks
    result := true;
  end;
end;

This function only tests the destination tile but it would be logical to test the eight adjacent tiles as well. If you want to do hypothetical searches without a specific unit you could pass a model to the function instead.

Minimise the maximum path distance

A key parameter for a pathfinder is the maximum path distance to allow. For sGetMoveAdvice this is MoveAdviceData.MoreTurns which sets the maximum allowed number of turns the unit can move along the path. The time cost of the search will be proportional to this parameter so you should minimise it. You can estimate a reasonable distance based on the distance between locations.

//Get the distance in tiles (TargetLocation is a CLocation).
MaxDistanceAllowed := TargetLocation.Distance(Un[uix]);
//Increase it to allow for obstacles and hard terrain (see also Iterative Deepening)
MaxDistanceAllowed := MaxDistanceAllowed * 4;
//Convert it to movement points (100 is the base cost of moving one tile).
MaxDistanceAllowed := MaxDistanceAllowed * 100;
//if necessary, generate maximum number of turns
MaxTurnsAllowed := MaxDistanceAllowed div Un[uix].common.speed;

If you are using the AI Lib version of AIClasses note that it's distance function returns the distance squared. So you need to take the square root to find the scalar distance.

Iterative Deepening

Setting the maximum distance is a hard problem. You want to set it as low as possible to minimise the cost but you don't want to set it too low - otherwise a valid path slightly longer might be excluded.

Iterative deepening is a general technique to reduce the cost of searches. You start by setting a low limit for the maximum and if the search fails, repeat it with a higher limit. You can end the loop when the search succeeds or when it is proved that there is no route or when the distance reaches an upper limit.

max := lower;	//initialise maximum distance to lower limit.
repeat
  path := Search(max);
  max := max * 2;		//increase maximum distance
until path.found or path.impossible or (max > upper)

Iterative deepening matches well with incremental pathfinders - pathfinders which save previous searches so that subsequent searches take less time. TBufferedGotoAdviser in AI Lib (gotoadvice.pas) is an incremental pathfinder.

For the lower limit you could use the movement the unit can achieve this turn (RO.Un[uix].movement) or you could estimate a minimum based on distance.

//Minimum distance if the path is a straight line and every tile has a railroad.
lower := (TargetLocation.Distance(Un[uix]) * Un[uix].common.speed * 2 div 25);
For sea and air units you can estimate based on the distance
//Minimum distance if the path is a straight line. (Canals?)
lower := (TargetLocation.Distance(Un[uix]) * 100);

These lower limits are very optimistic - you should start with a limit which assumes some diagonal tiles and some rough terrain so that the majority of searches are within the limit.

The upper limit is the maximum distance you want to allow - you probably want to specify the maximum number of turns to allow to move along the path.

upper := Un.common.speed * maxTurns;

This is approximate because movement is not conserved. With 0.8 movement rules, movement appears to be destroyed but not created.

Buffering Paths

A drawback of sGetMoveAdvice is that it only returns the portion of the path which the unit can complete this turn. This means that for long paths you will need to call sGetMoveAdvice every turn until the unit has completed the path. And long paths are the most expensive to generate (albeit the remaining path is shorter for each call).

If the path were saved when it was generated you could use it on subsequent turns without needing to call the pathfinder again. You would need to associate the path data with the unit (and the unit needs to know what step it is upto in the path).

If your AI is based on TBasicAI (AI Lib) you can derive unit control code from TAutoUnit (units\autounit.pas) which implements buffering automatically.

You can add buffering to your pathfinder if the full path is accessible. For sGetMoveAdvice you would need to copy the server code from core.pas to implement a custom version but it could be easier to write your own pathfinder. You could customise the AI Lib pathfinder, TGotoAdviser (unbuffered) or TBufferedGotoAdviser (gotoadvice.pas).

There are two disadvantages of buffering paths:

Suppose your AI is loaded on a turn when one of the units was following a buffered path. If the path wasn't saved (ie to the save file) then it must be recalculated. If the recalculated path could be different (to the original) then you must save the original path when it is first generated (and load it) to make your AI repeatable. Most path finders could recalculate a different path so any multi-turn buffers should be saved.

The map can change due to unknown terrain being explored or tile improvements (canals, roads, rails) changing or terrain terraforming or ZOCs changing...

The other disadvantage of buffering is that because you are not recalculating the path every turn, you can't take into account changes in the map so the path could be less than optimal or even impassable.

You can trade-off speed against update frequency (higher update frequency means faster response to map changes) by only buffering a few turns of steps. If you buffer two turns worth of steps, half the pathfinder calls are eliminated (for long paths) and the path will adapt to map changes every two turns. (The amount of save data is approximately constant because you save the buffer not the whole path.)

Alternatively you could make the number of buffered steps proportional to the cost of generating the search. This means buffer many steps when the path is long (and expensive to generate) but fewer as it shortens (and becomes cheaper to generate) eg buffer half the steps in the path if it requires more than one turn.

Common Target Optimisation

If you could detect that more than one search has the same target destination you could benefit by combining the searches. The combined search has cost proportional to the slowest individual search (for breadth-first search) whereas a sequence of individual searches costs the sum of the cost for each search.

You can implement this by searching outwards from the target until a path to each unit is found or all the paths finish.

The benefit from this depends on the ability/inclination to combine searches and the frequency of searches with common targets. Each unit model could have a different speed (and be a different domain). Combining searches with conflicting speed (movement is not conserved and railroad cost depends on speed) or domain would reduce path quality unless you have a mapping function to convert them - you can still gain if the mapping function is faster than a search. Obviously there can't be a mapping between sea and land searches but there could be a mapping between searches for different speed units of equivalent domains.

An optimal path depends on the remaining movement of the unit. This is the same as speed at the start of the turn but changes during a turn (eg after an attack). If you need to generate paths for units which have already moved but still have some movement then you need to take into account remaining movement (ie if you combine searches for units with different remaining movement you can't guarantee optimal paths). A mapping function could look for a short precursor path which would make the current unit have the same remaining movement.

TBufferedGotoAdviser (gotoadvice.pas in AI Lib) implements the common target optimisation by storing the search data for each search in a turn and re-using it (effectively combining it) if there is a subsequent search with a common target location and unit speed. Only land paths are implemented. No mapping functions are implemented. Remaining movement is ignored - trading-off performance (path quality) for speed (number of searches which can be combined). All the search data is freed at the end of the turn because it would have to be saved for repeatability (or updated for map changes) if it was kept longer than the current turn.

How to predict unknown tiles?

This is a prediction problem. You want a function which takes the known map as input and returns a predicted map.

Examination of the map generating functions (core.pas, CreateElevation, CreateMap) reveals that map generation is (more or less) random. This means that the optimal map predictor may only be able to give probabilities for each property of the map.

Given the landmass you can predict at least 50% of sea (ocean, shore) vs land tiles correctly. If landmass is greater than 50% predicting that unknown tiles are land will be correct for landmass% of tiles. If landmass is less than 50% predicting that unknown tiles are sea will be correct for 100-landmass% of tiles. In the middle case (landmass=50%) either prediction will be correct for 50% of tiles.

If some of the map is known you can adjust the land probability. If the known map includes all of the expected land you can conclude that the unknown regions must be sea.

The improvement is proportional to the proportion of the map which is known.

//Function to return the adjusted probability of land
//given a partially known map and landmass.
//call this before predicting the map.
function Pland(known :pTileList) :double;
var
  location,
  land,
  sea,
  KnownTiles :integer;
  ExpectedLand,
  UnknownLand :double;
begin
  land := 0; sea := 0;
  //Count the number of known tiles which are land or sea.
  for location := 0 to g.lx * g.ly - 1 do //for each known tile
    if known[location] and fTerrain <> fUnknown then
      if known[location] and fTerrain > fShore then //detect if land
        inc(Land);
      else
        inc(Sea);
  //Given the landmass (unconditional probability of a land tile)
  //and the known map, generate the probability of a land tile given the known map.
  KnownTiles := land + sea;
  ExpectedLand := g.lx * g.ly * g.landmass / 100;
  UnknownLand := ExpectedLand - land;

  if UnknownLand <= 0 then
    result := 0
  else
    if (KnownTiles = g.lx * g.ly) then  //whole map known
      result := land / g.lx * g.ly //academic
    else
      result := UnknownLand / (g.lx * g.ly - KnownTiles);
end;

The probability of predicting land correctly for a set of tiles is (L=UnknownLand, T=UnknownTiles)

P(Predict N land tiles correctly)= L/T * (L-1)/(T-1) * (L-2)/(T-2) ... * (L-(N-1))/(T-(N-1))
                                 = (L!/(L-N)!)/(T!/(T-N)!)

Which is approximately L/T raised to the power N. Alternatively, because landmass is a prior probability across the whole map it applies to any subset also, so you can predict land/sea in a subset of tiles with the same proportion of tiles correct as for the map.

Other information you could use to improve the prediction:

How to associate long-term data with an enemy unit or city or model?

You need to generate a unique identifier from the information you have about the enemy object using a mapping function. You store a copy of the identifier with the data to label it. The information used for the identifier should change infrequently (you must detect when it changes and update your identifier) and uniquely identify each object.

Conveniently the index of each item in EnemyUn, EnemyCity and EnemyModel is constant during the current turn.

For TModelInfo the identifier is the Owner and Mix (Owner+Mix shl 4).
For TCityInfo the identifier is the location.
For TUnitInfo the identifier is the location and Owner and Mix (Location+Owner shl 16 + Mix shl 20). You can add Health and Experience and Job and Flags for greater discrimination (but requires more code to detect when they change).

For these example identifiers I have used simple additive mapping functions, shifting (raising to a binary power) each component of the identifier to prevent overlaps. There can be upto 15 players, "shl 4" is *16 but faster to calculate. "shl 16" is *65536 - more than the maximum location. "shl 20" is *16 greater. For an identifier with more fields, eg a more discriminating TUnitInfo identifier, you could need to use multiplications instead of shifts and might need to switch to a hash function (overlapping fields). Alternatively you could store the identifier as a group of component values which is a copy of the enemy object's values (a little more expensive to test equality).

Units have location as part of their identifier. The identifier changes each time the unit moves so you must detect moves (if possible) and update the identifier. If the enemy unit moves out of observation range you can have a list of enemy unit data items with unknown location (but known last location and other data) and try to allocate them to newly observed enemy units.

For units the identifiers can be identical - in this case you can't distinguish which unit is which but it makes no difference to the game (you need to make sure you avoid setting all the identical units to use one data item or vice versa). You could also use index in RO.EnemyUn[] (in the unit identifier mapping function) but that is difficult to keep track of (remember that you have to update the data label every time the object's identifier changes [to find the data you need to know the old identifier]).

If you have a list of enemy data items you can find the data for an enemy object by searching through the list for the data item with the right identifier.

I think you need to save the identifiers each time they change (for repeatability).

How to find the enemy unit or city or model for my data?

If your data has a label for the enemy object you can find it 's associated game object by searching RO.EnemyUn[] or RO.EnemyCity[] or RO.EnemyModel[] for an object with the same label.

The index of each item in EnemyUn, EnemyCity and EnemyModel is constant during the current turn. Therefore you could store the index (or a pointer) with the data and use it when you want to find the item.

Enemy units can change their identifier frequently. It's easy for them to move outside the range of observation (unless you have the MIR Wonder) creating uncertainty.

How to detect the start and end of a human player's turn?

In a two-player game against a human you can deduce that it is their turn when it is not your turn.

If all the AI players are controlled by the same module you can detect when it is the turn of a non-AI player.

In a game with more than two players where between none and most players are human you could detect that the computer is less busy during human player's turns. AIs will tend to use 100% of available processing resources whereas a human will pause between actions. On a busy computer it could be hard to notice the difference.

How to implement a system for AI-players to communicate?

It's hard to implement a practical communication system without server support. It is better to implement it as an extension of the server eg as server commands (core.pas implements the Server function). Once your extension is well-debugged you can contact AI Developers to see if they like it and Civ Evo Administrators to see how keen they are to include it in the official version.

The reason it is hard to implement a communication system without server support is that the basic information you could communicate is data from your RO (and data generated from RO via server information requests). There is a problem in that if you sent data from your RO to another player they could not generate additional data from it via server information requests. This means you must send that information (generated via server information requests) in case they need it. With server support the player you were communicating with would automatically get the data you communicated in their RO - so they can call server information requests on it if they want to.

Another problem is that most AIs are designed to use RO. They can't take advantage of additional information (other player's RO data) without ripping up their code and rewriting it. (You can see here how convenient server support would be - communicated information could be transparently added to RO). You would need to provide an AugmentedRO which somehow merges communicated data and the current RO (not an easy task).

The Civ Evo source code can be hard to read but I assure you that it is far easier to add a few commands to the Server function than to struggle to implement communication on the client side.

There are a few practical problems: Should you allow untrue or speculative messages? (Can the player decide which received messages to apply to RO?) Are messages saved automatically? Will it take Megabytes if there are many players?

And something to think about: How will AIs communicate with humans?

Top