AI Recruitment

From The Battle for Wesnoth Wiki
Revision as of 13:03, 13 September 2013 by Flixx (talk | contribs) (Some notes on multiple leaders)

This page is work in progress

The new Recruitment Candidate Action is highly configurable and supports multiple leader recruiting...

How it works

Map analyis

In a first step the CA will analyse the map and try to find "important hexes". Those are locations on the map where a fight will occur with high probability. The important hexes are used later in combat simulations to calculate a average defense for the combatants.

If the game runs in debug mode the important hexes will be displayed with a white circle on them.

Implementation details for interested readers: [...]

Score Map

For all leaders which are available for recruiting a score map will be created including all unit-types from the recruit- and recall lists.

In the end the scores shall represent the desired unit-ratios on the map. (So the AI will not just recruit the unit with the highest score but the unit which approaches the wished mix best.) The scores are filled with values coming from combat analysis (see below).

After this the AI modifies the scores further in several ways:

  • Similar units get a penalty for being similar. Similar units are units in one advancement tree.
Example (Archer can advance to Ranger):
			before	after
Elvish Fighter: 	  50	 50
Elvish Archer:		  50	 25
Elvish Ranger:		  50	 25
  • The aspects recruitment_more, recruitment_diversity and recruitment_randomness are handled (see below).

Combat Analysis

For each unit-type our leaders can recruit the CA will simulate a fight with all enemy units on the map (If there are less then 5 enemy units, the enemies recruitment list(s) will be taken into account)

After Combat Analysis the score map could look like this:

Goblin Spearmanscore:		16.1252
Naga Fighter score:		0
Orcish Archer score:		61.6442
Orcish Assassin score:		88.0737
Orcish Grunt score:		100
Troll Whelp score:		26.7646
Wolf Rider score:		0

Implementation details for interested readers

Combat Analysis will use the function compare_unit_types(A, B). It takes two unit-types, simulates a fight (it uses a cache, because simulation is expensive) and returns a positive value if unit-type A is better then B and a negative value if unit-type B is better then A. If the return value is 2.0 it means that unit-type A is twice as good as unit-type B.

Here is a important code-snipped of compare_unit_types():

damage_to_b comes from simulations and is the average damage dealt by A when A attacks (A chooses weapon) plus the average damage dealt by A when B attacks (B chooses weapon). damage_to_a is equivalent.

double value_of_a = damage_to_b / (b_max_hp * a_cost);
double value_of_b = damage_to_a / (a_max_hp * b_cost);

if (value_of_a > value_of_b) {
  return value_of_a / value_of_b;
} else if (value_of_a < value_of_b) {
  return -value_of_b / value_of_a;
} else {
  return 0.;
}

The return-value of compare_unit_types() will be multiplied by the enemies current hp and added to the score map.

Included in the damage calculations are average defense (coming from "important hexes"), resistance, resistance abilities, average time of day, drain, poison, berserk, swarm and slows.

At this point the scores could be quite unhandy. They could all be negative, very high or to similar. So the scores will be transformed linear. The resulting scores are between 0.0 and 100.0.

Simplified code-snipped:

double new_100 = max_score;
double new_0 = max_score - (1.5 * (max_score - average_score));

BOOST_FOREACH(double& score, scores) {
  score = 100 * ((score - new_0) / (new_100 - new_0)); // linear transformation
  if (score < 0.) {
    score = 0.;
  }
}

Aspect recruitment_instruction

This is a powerful aspect to completely control recruitment via WML. Internally after the score-maps are created the CA will start to execute 'jobs' (or [recruit]-tags). If there is no job, the AI will do nothing at all.

This aspect is quite complex and requires the composite form of an aspect.

[ai]
  [aspect]
    id=recruitment-instruction
    [facet]
       turns=
       [value]
         #(here multiple [recruit] and/or [limit] tags)
         [recruit]
           #(attributes)
         [/recruit]
         [limit]
           #(attributes)
         [/limit]          
       [value]
    [/facet]
  [/aspect]
[/ai]

Attributes inside [facet]:

  • turns="": (string) This key takes a comma separated list containing number specifying the turn. '-' can be used between two values to define a range. A empty String means all turns.
  • See here for more. But note, that invalidate_on_gamestate_change and invalidate_on_minor_gamestate_change will have no effect on [recruit]-tags. They will be read at the beginning of the turn and are then immutable.

Attributes inside [recruit]: (with default values)

  • type="": (string) This key takes a comma separated list containing the unit-type, usage or level. Common usages are: 'scout', 'fighter', 'archer', 'healer' and 'mixed fighter'. A empty string means all units. If more then one unit is specified the AI will decide according to the score map what to recruit.
  • number=-1: (integer) A number greater than 0 will tell the AI to recruit n units. -1 means as much as possible. 0 means do not recruit.
  • importance=1: (integer) The importance of a recruitment tells the AI first to recruit units with the highest importance. If gold is lacking or the castle is full, only the most important units will be recruited, the other [recruit]-jobs will be dropped then.
  • leader_id="": (string) ID of the leader which shall execute this job. Empty sting means all leaders. (Note: This is only a recommendation for the AI. If the specified leader has no free hexes the AI will use other leaders to do the job. This is because the function check_recruit_action() in contexts.hpp will automatically choose another leader if the specified hasn't any free hexes)
  • total=no: (boolean) When total is set to yes the AI will count the own units on the map which matches at least one of the given types and will then recruit the difference between number and the counted amount.
  • blocker=yes: (boolean) If set to yes, the AI will stop recruiting when this job cannot be done (because of lacking gold or a [limit] for example). If set to no the AI will drop this job and try to continue with less important ones.
  • pattern=no: (boolean) If set to yes, the unit to recruit will not be chosen by the AI but randomly according to the frequency in the type attribute. For example when "type=Orcish Grunt, Orcish Grunt, scout" and "number=6", the AI will recruit 6 units whereas the probability that one unit is a Grunt is twice as big as the probability that the AI will recruit a scout. If the type-attribute is empty the AI will recruit randomly. (See also recruitment_pattern)

Attributes inside [limit]: (with default values)

  • type="": (string) This key takes a comma separated list containing the unit-type, usage or level. A empty string means all units.
  • max=0: (int) The maximum of units of the given type.

Notes:

  • [limit] has higher priority than [recruit].
  • The aspect recruitment_save_gold has higher priority then [recruit]. So if exact recruiting is wished, deactivate recruitment_save_gold (see below)
  • By default when recruitment is specified for a turn, the AI will not recruit any other unit. (So if we say recruit 1 scout in turn 1 then the AI will only recruit 1 scout and not more). To prevent this behavior one can add this [recruit]-tag:
[recruit]
   importance=0
[/recruit]

According to all the default values above (all types, as much as possible, ...) the AI will now fall back when all other recruitment jobs are done. This is also the content of the [default]-facet for the aspect.

  • Only one facet at a time can be active. When there are more [facet]s defined in a turn the behavior is undefined. In one [facet] can be many [recruit] or [limit]-tags.
  • Do not define more than one [recruit]-tags with the same importance. Note that the aspect recruitment_pattern will expand into a [recruit]-tag with importance=1.

Examples

Recruit 3 Grunts (and nothing more) in turn 3-5.

[ai]
  [aspect]
    id=recruitment-instructions
    [facet]
      turns=3-5
      [value] 
        [recruit]
          type=Orcish Grunt
          number=3
        [/recruit]
      [/value]
    [/facet]
  [/aspect]
[/ai]

Recruit so, that there are at least 4 scouts in total and then recruit other units.

[ai]
  [aspect]
    id=recruitment-instructions
    [facet]
      [value] 
        [recruit]
          type=scout
          number=4
          total=yes
          importance=1
        [/recruit]
        [recruit]
          importance=0
        [/recruit]
      [/value]
    [/facet]
  [/aspect]
[/ai]

Recruit 6 Grunts or level 2 units (whatever seems better for the AI) and nothing more.

[ai]
  [aspect]
    id=recruitment-instructions
    [facet]
      [value] 
        [recruit]
          type=Orcish Grunt, 2
          number=6
        [/recruit]
      [/value]
    [/facet]
  [/aspect]
[/ai]

Recruit 5 scouts with leader1 and 5 Grunts with leader 2. Do even try to recruit the Grunts if leader1 can not recruit all of the scouts.

[ai]
  [aspect]
    id=recruitment-instructions
    [facet]
      [value] 
        [recruit]
          type=scout
          number=5
          importance=2
          leader_id=leader1
          blocker=no
        [/recruit]
        [recruit]
          type=Orcish Grunt
          number=5
          importance=1
          leader_id=leader2
        [/recruit]
      [/value]
    [/facet]
  [/aspect]
[/ai]

Recruit always random units.

[ai]
  [aspect]
    id=recruitment-instructions
    [facet]
      [value] 
        [recruit]
          type=
          pattern=yes
          importance=1
        [/recruit]
      [/value]
    [/facet]
  [/aspect]
[/ai]

Do not recruit in turn 4.

[ai]
  [aspect]
    id=recruitment-instructions
    [facet]
      turns=4
      [value] 
        [recruit]
          importance=1
          number=0
        [/recruit]
      [/value]
    [/facet]
  [/aspect]
[/ai]

Do not have more than 6 units on the map.

[ai]
  [aspect]
    id=recruitment-instructions
    [facet]
      [value] 
        [limit]
          type=    #empty type means all units
          max=6
        [/limit]
      [/value]
    [/facet]
  [/aspect]
[/ai]

This will not work. Only one facet can be active.

[ai]
  [aspect]
    id=recruitment-instructions
    [facet]
      turns=1-10
      [value] 
        [recruit]
          type=scout
          number=5
          importance=1
        [/recruit]
      [/value]
    [/facet]
    [facet]
      turns=3
      [value] 
        [recruit]
          type=Orcish Grunt
          number=3
          importance=2
        [/recruit]
      [/value]
    [/facet]
  [/aspect]
[/ai]

To achieve this behavior one have to create a facet for turn 1-2, a facet for turn 3 and another facet for turn 4-10.

Aspect recruitment_save_gold

How it works

Sometimes it's a good idea not to spend all gold immediately for several reasons:

  • To save the upkeep,
  • To wait for the enemy to recruit first to counter easily,
  • To save money for the next scenario when the enemy is almost defeated.

At the same time it is a good idea (especially for the AI) to recruit in "waves" (in contrary to recruit one unit each turn).

For this we have the aspect "recruitment_save_gold".

The Recruitment CA is always in one of the following states:

  • NORMAL,
  • SAVE_GOLD,
  • SPEND_ALL_GOLD,
  • LEADER_IN_DANGER.

Except the state LEADER_IN_DANGER, the states are persistent over turns.

The AI keeps track of a "unit_ratio". This is our_total_unit_costs / enemy_total_unit_costs whereas the costs are the sum of the cost of all units on the map weighted by their HP. Also the AI tries to estimate the total income over the next 5 turns. (This is a quite rough estimation, but that's fine.)

When the AI is in state NORMAL and the unit_ratio exceeds a predefined threshold ("begin") and the estimated income is positive, the AI will switch to the state SAVE_GOLD. Now the AI doesn't recruit units anymore until the unit_ratio falls below another predefined threshold ("end"). Then the AI will switch to the NORMAL state and recruit again.

One can imagine that the AI can save quite a lot of gold with this behavior. But saving gold forever is senseless. So when the AI has more gold then a predefined value ("spend_all_gold"), it will switch into the state SPEND_ALL_GOLD and recruit a big army to start a offensive wave.

The AI will ignore all those gold saving strategies when there is a LEADER_IN_DANGER. That happens when a enemy is near the leader (3 hexes).

The Aspect

This aspect requires also the composite form.

[ai]
  [aspect]
    id=recruitment_save_gold
    [facet]
      [value]
        #(attributes)
      [/value]
    [/facet]
  [/aspect]
[/ai]

Attributes inside [value] (with default values)

  • active=yes: (boolean) If active=no the AI will always recruit if gold is available.
  • begin=1.0: (double) See explanation above.
  • end=0.7: (double) See explanation above.
  • spend_all_gold=-1: (int) See explanation above. If set to -1 the value will automatically set to the teams start gold + 1.

Example

This example is filled with all default values and there for quick coping.

[ai]
  [aspect]
    id=recruitment_save_gold
    [facet]
      [value]
        active=yes
        begin=1.0
        end=0.7
        spend_all_gold=-1
      [/value]
    [/facet]
  [/aspect]
[/ai]

Other aspects

  • recruitment_diversity=1.0 (double) When this value is high, the AI will recruit more units which are currently rare on the map. (recruitment_diversity * 25 will be added to each score).
  • recruitment_more="" (string) This key takes a comma separated list containing unit-types, usages or levels. This is meant to let a scenario editor make a easy hack when he/she wants the AI to recruit more units of a specific type. (25 will be added to the unit-types score). recruitment-more="Orcish Grunt, Orcish Grunt" is possible to add 50.
  • recruitment_randomness=15 (int) To each score a random value between 0 and recruitment_randomness will be added. Don't hesitate to use a high value (like 200) to increase randomness.
  • recruitment_pattern="" (string) This key takes a comma separated list containing unit-types, usages or levels. Common usages are: 'scout', 'fighter', 'archer', 'healer' and 'mixed fighter'. This tells the AI with what probability it should recruit different types of units. The usage is listed in the unit type config files (see data/core/units/ for mainline units; see also UnitTypeWML).
    • For example, recruitment_pattern=fighter,fighter,2 means that the AI recruits on average twice as many fighters as level 2 units. It does not mean that it recruits two fighters first, then a level 2 unit, then two fighters again, etc.
    • Internally recruitment_pattern will expand into a recruitment_instruction with pattern=yes and importance=1. (So recruitment_pattern is a shortcut for a special recruitment_instruction). Make sure that there is no recruitment_instruction with importance=1 when using recruitment_pattern.
  • villages_per_scout=4 (int) A number 0 or higher which determines how many scouts the AI recruits. If 0, the AI doesn't recruit scouts to capture villages.

Some notes on multiple leaders

The CA can recruit with multiple leaders - even with different recruitment lists. Also the CA 'Move_Leader_To_Keep' will try to move all units with canrecruit=yes to the next keep.

However there are some drawbacks:

  • In the current version the leaders will recruit by default almost the same amount of units. This can partly configured with the aspect recruitment_instruction and it's attribute leader_id. A leader will recruit more units when he/she is in danger (enemies are near).
  • As mentioned above leader_id in recruitment_instruction is only a recommendation.
  • Multiple leaders with different recruitment lists in combination with a recruitment_pattern may be confusing. (For the AI it's more important that the leaders recruit the same amount of units then fulfilling the ratios given in the recruitment_pattern.