Difference between revisions of "AI Recruitment"

From The Battle for Wesnoth Wiki
(Aspect recruitment_instruction)
(Aspect recruitment_instructions: Add some anchors that can be linked to)
 
(79 intermediate revisions by 7 users not shown)
Line 1: Line 1:
'''This page is work in progress'''
+
A new [[RCA_AI#The_Candidate_Actions_.28CAs.29_of_the_main_loop_Stage_in_the_RCA_AI|recruitment candidate action]] (CA) was introduced and made the default in Wesnoth 1.11.  It is much more configurable than previous versions and supports multiple leader recruiting.  This page describes how it works and the parameters available for customizing it.
  
The new Recruitment Candidate Action is highly configurable and supports multiple leader recruiting.
+
==How it works==
 +
===Map analysis===
 +
In a first step, the CA analyzes the map and tries to find "important hexes". Those are locations on the map where a fight will occur with high probability. The important hexes are later used in combat simulations to calculate an average defense rating for the combatants.
  
=How it works=
+
When the game is running in debug mode ('''must''' be started with <code>--debug</code>, enabling debug mode in-game is not the same) ''and'' the game is started with the parameter <code>--log-info=ai/recruitment</code> the important hexes are marked with white 'x' markers on the map.
==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 later used in combat simulations to calculate an average defense for the combatants.
 
  
When the game is running in debug mode the important hexes are marked with a white dot.
+
===Score Map===
  
==Score Map==
+
A score map is created for all leaders who are able to recruit. All unit types from the recruit and recall lists are mapped to a score.  
A score map will be created for all leaders who are able to recruit. All unit-types from the recruit- and recall lists are mapped to a score.  
 
  
In the end the scores represent the desired unit-ratios on the map. (This means the AI will ''not'' just recruit the unit with the highest score, but in such a way that the mix of units on the map comes as close as possible to the mix represented by the scores.)
+
In the end, the scores represent the desired unit ratios on the map. This means the AI will ''not'' just recruit the unit with the highest score, but in such a way that the mix of units on the map comes as close as possible to the mix represented by the scores.
The scores are filled with values coming from combat analysis (see below).
+
The scores are filled with values coming from combat analyses (see below).
  
 
After that the AI modifies the scores in several ways:
 
After that the AI modifies the scores in several ways:
Line 22: Line 21:
 
  Elvish Archer:   50 25
 
  Elvish Archer:   50 25
 
  Elvish Ranger:   50 25
 
  Elvish Ranger:   50 25
* The aspects <code>recruitment_more</code>, <code>recruitment_diversity</code> and <code>recruitment_randomness</code> are handled ([[#Other_aspects|see below]]).
+
* The aspects ''recruitment_more'', ''recruitment_diversity'' and ''recruitment_randomness'' are handled ([[#Other_Recruitment_Related_Aspects|see below]]).
  
==Combat Analysis==
+
===Combat Analysis===
For each unit-type the AI's leaders can recruit, the CA will simulate a fight against all enemy units on the map. If there are less then 5 enemy units, the enemies recruitment list(s) will be taken into account.
+
For each unit type the AI leaders can recruit, the CA simulates fights against all enemy units on the map. If there are less then 5 enemy units, the enemy recruitment list(s) are also taken into account.
  
After Combat Analysis the score map may look like this:
+
After combat analysis the score map may look like this:
  
 
  Goblin Spearman score: 16.1252
 
  Goblin Spearman score: 16.1252
Line 37: Line 36:
 
  Wolf Rider score: 0
 
  Wolf Rider score: 0
  
===Implementation details for interested readers===
+
====Implementation details for interested readers====
  
Combat Analysis will use the function <code>compare_unit_types(A, B)</code>. It takes two unit-types and simulates a fight using a cache because simulation is expensive. The function returns a positive value if unit-type <code>A</code> is better then <code>B</code> and a negative value if unit-type <code>B</code> is better then <code>A</code>. If the return value is 2.0 it means that unit-type A is twice as good as unit-type B.
+
The combat analysis uses the function ''compare_unit_types(A, B)''. It takes two unit types and simulates a fight, using a cache because simulation is expensive. The function returns a positive value if unit type ''A'' is better than ''B'' and a negative value if unit type ''B'' is better than ''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 <code>compare_unit_types()</code>:
+
Here is a important code snippet from ''compare_unit_types()'':
 +
<syntaxhighlight lang=cpp>
 +
double value_of_a = damage_to_b / (b_max_hp * a_cost);
 +
double value_of_b = damage_to_a / (a_max_hp * b_cost);
  
<code>damage_to_b</code> comes from simulations and is the average damage dealt by <code>A</code> when <code>A</code> attacks (<code>A</code> chooses weapon) plus the average damage dealt by <code>A</code> when <code>B</code> attacks (<code>B</code> chooses weapon). <code>damage_to_a</code> is equivalent.
+
if (value_of_a > value_of_b) {
double value_of_a = damage_to_b / (b_max_hp * a_cost);
+
  return value_of_a / value_of_b;
double value_of_b = damage_to_a / (a_max_hp * b_cost);
+
} else if (value_of_a < value_of_b) {
+
  return -value_of_b / value_of_a;
if (value_of_a > value_of_b) {
+
} else {
  return value_of_a / value_of_b;
+
  return 0.;
} else if (value_of_a < value_of_b) {
+
}
  return -value_of_b / value_of_a;
+
</syntaxhighlight>
} else {
+
''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.
  return 0.;
 
}
 
  
The return-value of <code>compare_unit_types()</code> will be multiplied by the enemies current hp and added to the score map.
+
The return value of ''compare_unit_types()'' is multiplied by the enemy's current hitpoints 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.
 
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 can be quite unhandy. They could all be negative, very high or too similar. So the scores will be transformed linearly. The resulting scores are numbers between 0.0 and 100.0.  
+
At this point the scores can be quite inconvenient for a generalized treatment. They could all be negative, very high or too similar. Thus, the scores are transformed linearly. The resulting scores are numbers between 0.0 and 100.0.
 +
The minimum depends on the aspect ''recruitment_diversity''.
  
Simplified code-snipped:
+
Simplified code snippet:
double new_100 = max_score;
+
<syntaxhighlight lang=cpp>
double new_0 = max_score - (1.5 * (max_score - average_score));
+
double new_100 = max_score;
+
double new_0 = max_score - (recruitment_diversity * (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=
+
for (double& score : scores) {
This is a powerful aspect to completely control recruitment via WML. After the score maps are created the CA will start to execute 'jobs' (or <code>[recruit]</code>-tags). If there is no job, the AI will do nothing at all.
+
  score = 100 * ((score - new_0) / (new_100 - new_0)); // linear transformation
 +
  if (score < 0.) {
 +
    score = 0.;
 +
  }
 +
}
 +
</syntaxhighlight>
  
This aspect is quite complex and requires the [[AiWML#A Bit More on_Simple vs. Composite Aspects|composite form of an aspect]].
+
==Aspect ''recruitment_instructions''==
[ai]
 
  [aspect]
 
    id=recruitment-instruction
 
    [facet]
 
        turns=
 
        [value]
 
          <span style="color:#999999">#(here multiple [recruit] and/or [limit]-tags)</span>
 
          [recruit]
 
            <span style="color:#999999">#(attributes)</span>
 
          [/recruit]
 
          [limit]
 
            <span style="color:#999999">#(attributes)</span>
 
          [/limit]         
 
        [value]
 
    [/facet]
 
  [/aspect]
 
[/ai]
 
  
Attributes inside <code>[facet]</code>:
+
This is a powerful aspect for controlling recruitment via WML.  Its syntax is quite complex and requires the [[Wesnoth_AI_Framework#The_.5Bai.5D_Tag_.E2.80.94_Aspects|''composite'' aspect configuration]]. At its core are the ''[recruit]'' tags, which are the instructions to the candidate action defining which units are to be recruited.
* '''turns="": (string)''' This key takes a comma separated list containing numbers specifying the turns. '-' can be used between two values to define a range. An empty String means ''all turns''.
+
<syntaxhighlight lang=wml>
* See [[AiWML#A Bit More on_Simple vs. Composite Aspects|here]] for more information. But note, that <code>invalidate_on_gamestate_change</code> and <code>invalidate_on_minor_gamestate_change</code> will have no effect on <code>[recruit]</code>-tags. They will be read at the beginning of the turn and then be immutable.
+
[ai]
 +
  [aspect]
 +
    id=recruitment_instructions
 +
    [facet]
 +
      turns=
 +
      [value]
 +
        #(here multiple [recruit] and/or [limit]-tags)
 +
        [recruit]
 +
          #(attributes)
 +
        [/recruit]
 +
        [limit]
 +
          #(attributes)
 +
        [/limit]        
 +
      [value]
 +
    [/facet]
 +
  [/aspect]
 +
[/ai]
 +
</syntaxhighlight>
  
Attributes inside <code>[recruit]</code>: ''(with default values)''
+
Attributes inside '''[facet]''':
* '''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.
+
* '''turns'''="": (string) This key takes a comma separated list containing numbers specifying the turns. '-' can be used between two values to define a range. An empty string means ''all turns''.
* '''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''.
+
* The [[Wesnoth_AI_Framework#The_.5Bai.5D_Tag_.E2.80.94_Aspects|other keys possible in [facet] tags]] can, in principle, also be used, but note that ''invalidate_on_gamestate_change'' and ''invalidate_on_minor_gamestate_change'' have no effect on [recruit] tagsThey are read at the beginning of the turn and are immutable for the remainder of the turn.
* '''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 <code>[recruit]</code>-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 <code>[limit]</code> 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 [[#Other_aspects|recruitment_pattern]])
 
  
Attributes inside <code>[limit]</code>: ''(with default values)''
+
Attributes inside {{anchor|instructions-recruit|'''[recruit]'''}}:
* '''type="": (string)''' This key takes a comma separated list containing the unit-type, usage or level. A empty string means ''all units''.
+
* '''id''': (string) {{DevFeature1.13|5}} By specifying an ID it becomes possible to manipulate the [recruit] tags using [[DirectActionsWML#.5Bmodify_ai.5D|modify_ai]]. This is entirely optional.
* '''max=0: (int)''' The maximum of units of the given type.
+
* '''type='''"": (string) This key takes a comma separated list containing unit types, usages or levels. Common values for ''usage'' are 'scout', 'fighter', 'archer', 'healer' and 'mixed fighter'. An empty string means 'all units'. If more than one unit is specified, the AI decides what to recruit according to the score map.
 +
* '''number'''=-1: (integer) A number ''n'' greater than 0 instructs the AI to recruit ''n'' units. '-1' means 'as much as possible'. '0' means 'do not recruit'.
 +
* '''importance'''=1: (integer) Priority of the recruitment.  The AI first recruits units with the highest importance, until the specified limits are reached. If gold is lacking or the castle is full, only the most important units are recruited, all other [recruit] jobs are dropped.
 +
* '''leader_id'''="": (string) ''id'' of the leader to execute this recruitment. Empty string means 'all leaders'.  Note that this is only a recommendation to the AI. If the specified leader has no free hexes available, the AI uses other leaders to do the job.
 +
* '''total'''=no: (boolean) If set to 'yes', the AI counts the own units on the map which match at least one of the given types and then recruits the difference between ''number'' and the counted amount.
 +
* '''blocker'''=yes: (boolean) If set to 'yes', the AI stops recruiting if this job cannot be done (for example, because of lack of gold or a [limit]). If set to 'no', the AI drops this job and tries to continue with less important ones.
 +
* '''pattern'''=no: (boolean) If set to 'yes', the unit to recruit is not chosen by the AI, but randomly according to the frequency in the type attribute. For example, when <code>type=Orcish Grunt, Orcish Grunt, scout</code> and <code>number=6</code>, the AI recruits 6 units, with the probability that one unit is a grunt being twice as high as that of a scout. If the ''type'' attribute is empty, the AI recruits randomly. (See also [[#Other_Recruitment_Related_Aspects|''recruitment_pattern'']])
  
Notes:
+
Attributes inside {{anchor|instructions-limit|'''[limit]'''}}:
* <code>[limit]</code> has higher priority than <code>[recruit]</code>.
+
* '''id''': (string) {{DevFeature1.13|5}} By specifying an ID it becomes possible to manipulate the [limit] tags using [[DirectActionsWML#.5Bmodify_ai.5D|modify_ai]]. This is entirely optional.
* The aspect <code>recruitment_save_gold</code> has higher priority then <code>[recruit]</code>. So if exact recruiting is wished, deactivate <code>recruitment_save_gold</code> ([[#Aspect_recruitment_save_gold|see below]])
+
* '''type'''="": (string) This key takes a comma separated list containing unit types, usages or levels. An empty string means 'all units'.
* 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 <code>[recruit]</code>-tag:
+
* '''max'''=0: (int) The maximum of units of the given type(s).
[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 <code>[default]</code>-facet for the aspect.
 
* Only one facet at a time can be active. When there are more <code>[facet]</code>s defined in a turn the behavior is undefined. In one <code>[facet]</code> can be many <code>[recruit]</code> or <code>[limit]</code>-tags.
 
* Do not define more than one <code>[recruit]</code>-tags with the same importance. Note that the aspect <code>recruitment_pattern</code> will expand into a <code>[recruit]</code>-tag with importance=1.
 
  
==Examples==
+
'''Notes:'''
Recruit 3 Grunts (and nothing more) in turn 3-5.
+
* [limit] has higher priority than [recruit].
[ai]
+
* The ''recruitment_save_gold'' aspect has higher priority than [recruit]. So if exact recruiting is wished, deactivate ''recruitment_save_gold'' ([[#Aspect_recruitment_save_gold|see below]]).
  [aspect]
+
* If recruitment is specified for a turn, the AI does '''not recruit any other unit''' by default. (So if the AI is told to recruit 1 scout in turn 1, then the AI will only recruit 1 scout and no other units). To prevent this behavior one can add this [recruit] tag:
    id=recruitment-instructions
+
<syntaxhighlight lang=wml>
    [facet]
+
[recruit]
      '''turns=3-5'''
+
  importance=0
      [value]  
+
[/recruit]
        [recruit]
+
</syntaxhighlight>
          '''type=Orcish Grunt'''
+
The AI then falls back to normal recruiting when all other [recruit] tags are dealt with. This is also the content of the [default] facet for the aspect.
          '''number=3'''
+
* Even if only a [limit] tag is used in the aspect declaration, the [default] facet will get overwritten. Include the [recruit] tag mentioned in the previous point to let the AI recruit something. {{DevFeature1.13|5}} As of 1.13.5, if there is a [limit] tag but no [recruit] tag, an empty [recruit] tag is implied, allowing the AI to recruit. If both [limit] and [recruit] tags are present, no additional [recruit] tag is ever implied.
        [/recruit]
+
* Only one facet at a time can be active. When there are more [facet]s defined in a turn, only one will be used (usually the first one). Use several [recruit] and/or [limit] tags in the same facet instead.
      [/value]
+
* Do not define more than one [recruit] tag with the same importance. Also note that the aspect ''recruitment_pattern'' expands into a [recruit] tag with importance=1.
    [/facet]
+
* {{DevFeature1.13|5}} The tags '''[total]''' and '''[pattern]''' may be used instead of [recruit]. These have basically the same meaning and contents as [recruit], but imply either total=yes or pattern=yes. They are converted to [recruit] tags and can be manipulated as such by modify_ai.
  [/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]
 
  
 +
===Examples===
 +
Recruit 3 grunts (and nothing else) on turns 3-5.
 +
<syntaxhighlight lang=wml highlight=5,8,9>
 +
[ai]
 +
  [aspect]
 +
    id=recruitment_instructions
 +
    [facet]
 +
      turns=3-5
 +
      [value]
 +
        [recruit]
 +
          type=Orcish Grunt
 +
          number=3
 +
        [/recruit]
 +
      [/value]
 +
    [/facet]
 +
  [/aspect]
 +
[/ai]
 +
</syntaxhighlight>
 +
Recruit as many scouts as possible until there are 4 scouts in total, then recruit other units.
 +
<syntaxhighlight lang=wml highlight=9,12-14>
 +
[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]
 +
</syntaxhighlight>
 +
Recruit 6 grunts or level 2 units (whatever seems better to the AI) and nothing else.
 +
<syntaxhighlight lang=wml highlight=7>
 +
[ai]
 +
  [aspect]
 +
    id=recruitment_instructions
 +
    [facet]
 +
      [value]
 +
        [recruit]
 +
          type=Orcish Grunt, 2
 +
          number=6
 +
        [/recruit]
 +
      [/value]
 +
    [/facet]
 +
  [/aspect]
 +
[/ai]
 +
</syntaxhighlight>
 +
Recruit 5 scouts with 'leader1' and 5 grunts with 'leader2'. Try to recruit the grunts even if 'leader1' cannot recruit all of the scouts.
 +
<syntaxhighlight lang=wml highlight=9-11,16,17>
 +
[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]
 +
</syntaxhighlight>
 +
Always recruit random units.
 +
<syntaxhighlight lang=wml highlight=7,8>
 +
[ai]
 +
  [aspect]
 +
    id=recruitment_instructions
 +
    [facet]
 +
      [value]
 +
        [recruit]
 +
          type=
 +
          pattern=yes
 +
          importance=1
 +
        [/recruit]
 +
      [/value]
 +
    [/facet]
 +
  [/aspect]
 +
[/ai]
 +
</syntaxhighlight>
 +
Do not recruit on turn 4.
 +
<syntaxhighlight lang=wml highlight=5,9>
 +
[ai]
 +
  [aspect]
 +
    id=recruitment_instructions
 +
    [facet]
 +
      turns=4
 +
      [value]
 +
        [recruit]
 +
          importance=1
 +
          number=0
 +
        [/recruit]
 +
      [/value]
 +
    [/facet]
 +
  [/aspect]
 +
[/ai]
 +
</syntaxhighlight>
 
Do not have more than 6 units on the map.
 
Do not have more than 6 units on the map.
[ai]
+
<syntaxhighlight lang=wml>
  [aspect]
+
[ai]
    id=recruitment-instructions
+
  [aspect]
    [facet]
+
    id=recruitment_instructions
      [value]  
+
    [facet]
        [limit]
+
      [value]  
          type=    <span style="color:#999999">#empty type means all units</span>
+
        [limit]
          max=6
+
          type=    #empty type means all units
        [/limit]
+
          max=6
       [/value]
+
        [/limit]
    [/facet]
+
        [recruit]        #Do not forget to include the default
  [/aspect]
+
          importance=0    #[recruit]-tag. Otherwise the AI won't
[/ai]
+
        [/recruit]       #recruit anything.
 +
      [/value]
 +
    [/facet]
 +
  [/aspect]
 +
[/ai]
 +
</syntaxhighlight>
 +
The following does not work because only one [facet] can be active.
 +
<syntaxhighlight lang=wml highlight=5,10,15,20>
 +
[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]
 +
</syntaxhighlight>
 +
Instead, one has to use <code>turns=1-2,4-10</code> in the first facet.
 +
 
 +
==Aspect ''recruitment_save_gold''==
 +
 
 +
===How it works===
 +
 
 +
For several reasons it can be a good idea not to spend all gold at once:
 +
* To save upkeep
 +
* To wait for the enemy to recruit first and thus to counter more easily
 +
* To save gold for the next scenario when the enemy is almost defeated
 +
It can also be a good idea (especially for the AI) to recruit in waves (instead of recruiting one unit or so each turn).  If this type of behavior is desired, there is the aspect ''recruitment_save_gold''.
  
This will not work. Only one facet can be active.
+
The Recruitment CA is always in one of the following states:
[ai]
+
* NORMAL
  [aspect]
+
* SAVE_GOLD
    id=recruitment-instructions
+
* SPEND_ALL_GOLD
    [facet]
+
* LEADER_IN_DANGER
      '''turns=1-10'''
+
Except for state 'LEADER_IN_DANGER', the states are persistent over turns.
      [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=
+
The AI keeps track of a ''unit_ratio''. This is <code>our_total_unit_costs / enemy_total_unit_costs</code>, where the costs are the sum of the cost of all units on the map weighted by their HP.  The AI also attempts to estimate the total income over the next 5 turns.
==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).
+
If the AI is in state NORMAL '''and''' the unit_ratio exceeds a predefined threshold (defined in ''begin'', see below) '''and''' the estimated income is positive, the AI switches to state SAVE_GOLD. Now the AI does not recruit units anymore until ''unit_ratio'' falls below another predefined threshold (defined in ''end''). Then the AI switches back to state NORMAL and recruits again.
  
For this we have the aspect "recruitment_save_gold".
+
With this behavior the AI can save quite a lot of gold. If the gold then accumulates to more than another predefined value (''spend_all_gold''), it switches into state SPEND_ALL_GOLD and recruits a big army to start an offensive wave.
  
The Recruitment CA is always in one of the following states:
+
The AI ignores all those gold saving strategies when it is in state LEADER_IN_DANGER. This happens when an enemy is close to the AI leader (within 5 hexes).
* 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 <code>our_total_unit_costs / enemy_total_unit_costs</code> whereas the costs are the sum of the cost of all units on the map weighted by their HP.
+
'''Important note:''' While one might assume that the AI plays better when it tries to save some gold, tests show that this is generally not the case. They indicate that, for the current AI, it is always better for the AI to have as many units on the map as possible. This aspect is therefore deactivated by default.
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.
+
===The Aspect===
 +
This aspect also requires the [[Wesnoth_AI_Framework#The_.5Bai.5D_Tag_.E2.80.94_Aspects|composite syntax]].
 +
<syntaxhighlight lang=wml>
 +
[ai]
 +
  [aspect]
 +
    id=recruitment_save_gold
 +
    [facet]
 +
      [value]
 +
        #(attributes)
 +
      [/value]
 +
    [/facet]
 +
  [/aspect]
 +
[/ai]
 +
</syntaxhighlight>
 +
Attributes inside '''[value]''':
 +
* '''active'''=0: (int) From this turn on the aspect is active. The AI always spends all gold until then. <code>active=0</code> can be used to deactivate gold saving.
 +
* '''begin''': (double) See explanation above.
 +
* '''end''': (double) See explanation above.
 +
* '''spend_all_gold''': (int) See explanation above. If set to '-1', the AI never switches to state SPEND_ALL_GOLD.
 +
* '''save_on_negative_income''': (boolean) If this is set to yes, the AI switches to state SAVE_GOLD even if the income is negative (estimated over 5 turns, village gain and unit loss estimation included).
  
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.
+
===Example===
 +
<syntaxhighlight lang=wml>
 +
[ai]
 +
  [aspect]
 +
    id=recruitment_save_gold
 +
    [facet]
 +
      [value]
 +
        active=2
 +
        begin=1.5
 +
        end=1.1
 +
        spend_all_gold=-1
 +
        save_on_negative_income=no
 +
      [/value]
 +
    [/facet]
 +
  [/aspect]
 +
[/ai]
 +
</syntaxhighlight>
  
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).
+
A macro can be used to deactivate recruitment_save_gold:
  
==The Aspect==
+
<syntaxhighlight lang=wml>
This aspect requires also the [[AiWML#A Bit More on_Simple vs. Composite Aspects|composite form]].
+
{AI_ASPECT recruitment_save_gold {AI_DEACTIVATE_SAVE_GOLD} }
[ai]
+
</syntaxhighlight>
  [aspect]
 
    id=recruitment_save_gold
 
    [facet]
 
      [value]
 
        <span style="color:#999999">#(attributes)</span>
 
      [/value]
 
    [/facet]
 
  [/aspect]
 
[/ai]
 
Attributes inside <code>[value]</code> ''(with default values)''
 
* '''active=yes: (boolean)''' If <code>active=no</code> 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==
+
==Other Recruitment Related Aspects==
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'''=2.0: (double) When this value is high, the AI recruits more units which are currently rare on the map. When the value is 0.0, the AI only recruits the best unit. See [[#Implementation_details_for_interested_readers|above]] for implementation details.
*'''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 an easy hack for making the AI recruit more units of a specific type. (25 will be added to the unit type's score.) The same unit type can also be used several time. For example, with <code>recruitment_more="Orcish Grunt, Orcish Grunt"</code> it is possible to add a total of 50 to the Grunt's 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). <code>recruitment-more="Orcish Grunt, Orcish Grunt"</code> is possible to add 50.
+
*'''recruitment_randomness'''=50: (int) A random value between 0 and ''recruitment_randomness'' is added to each score. Use a high value, such as '200', to increase randomness.
*'''recruitment_randomness'''=15 (int) To each score a random value between 0 and <code>recruitment_randomness</code> 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 values for ''usage'' 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]]).
*'''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, <code>recruitment_pattern=fighter,fighter,2</code> 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.
 
**For example, <code>recruitment_pattern=fighter,fighter,2</code> 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 <code>recruitment_pattern</code> will expand into a <code>recruitment_instruction</code> with <code>pattern=yes</code> and <code>importance=1</code>. (So <code>recruitment_pattern</code> is a shortcut for a special <code>recruitment_instruction</code>). Make sure that there is no <code>recruitment_instruction</code> with <code>importance=1</code> when using <code>recruitment_pattern</code>.
+
**Internally ''recruitment_pattern'' expands into a ''recruitment_instructions'' facet with <code>pattern=yes</code> and <code>importance=1</code>. Make sure that there is no ''recruitment_instructions'' facet with <code>importance=1</code> 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.
+
*'''villages_per_scout'''=4: (int) A number 0 or higher which determines how many scouts the AI recruits. If set to '0', the AI does not recruit scouts to capture villages.
 +
 
 +
==Making recruitment strong==
 +
 
 +
The default values for the recruitment aspects are chosen to provide variety in the units the AI recruits.  Tests have shown that it plays slightly better (against other AIs, at least) when it mostly recruits the units that score highest in the evaluation explained above.  The best values found are:
 +
* '''recruitment_diversity=0.8''': Let the AI recruit the best units only.
 +
* '''recruitment_randomness=0''': Let the recruitment not be random.
 +
* '''villages_per_scout=0''': The AI plays slightly better with fewer or no scouts.
 +
* '''deactivate recruitment_save_gold''': The AI plays better when it spends all money.
 +
 
 +
In Multiplayer games, the AI 'Strong AI (RCA)' can be chosen with the settings as described here.  {{DevFeature1.13|5}}  As this is only a very minor modification of the default AI, this AI has been changed to a [[Wesnoth_AI#AIs_Available_in_the_Multiplayer_Setup_Menu|Dev AI]] (available in debug mode only) and has been renamed to 'Default AI (RCA) with Alternate Recruiting'.
  
 
==Some notes on multiple leaders==
 
==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 <code>canrecruit=yes</code> to the next keep.
 
  
However there are some drawbacks:
+
The new recruitment CA is able to recruit with multiple leaders — even with different recruitment lists for each leader.  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 <code>recruitment_instruction</code> and it's attribute <code>leader_id</code>. A leader will recruit more units when he/she is in danger (enemies are near).
+
* In the current version, the leaders recruit almost the same amount of units by default. This can partly be configured with the ''recruitment_instructions'' aspect and its attribute ''leader_id''.
* As mentioned above <code>leader_id</code> in <code>recruitment_instruction</code> is only a recommendation.
+
** Note however that, as mentioned above, ''leader_id'' in ''recruitment_instructions'' is only a recommendation. For example, a leader recruits more units when enemies are near.
* Multiple leaders with different recruitment lists in combination with a <code>recruitment_pattern</code> 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 <code>recruitment_pattern</code>.
+
* Multiple leaders with different recruitment lists in combination with a <code>recruitment_pattern</code> may not result in the expected behavior. For the AI it is more important that the leaders recruit the same amount of units than fulfilling the ratios given in ''recruitment_pattern''.
 +
 
 +
 
 +
'''See also:''' [[Wesnoth AI]]
 +
[[Category:AI]]

Latest revision as of 05:25, 27 February 2024

A new recruitment candidate action (CA) was introduced and made the default in Wesnoth 1.11. It is much more configurable than previous versions and supports multiple leader recruiting. This page describes how it works and the parameters available for customizing it.

How it works

Map analysis

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

When the game is running in debug mode (must be started with --debug, enabling debug mode in-game is not the same) and the game is started with the parameter --log-info=ai/recruitment the important hexes are marked with white 'x' markers on the map.

Score Map

A score map is created for all leaders who are able to recruit. All unit types from the recruit and recall lists are mapped to a score.

In the end, the scores represent the desired unit ratios on the map. This means the AI will not just recruit the unit with the highest score, but in such a way that the mix of units on the map comes as close as possible to the mix represented by the scores. The scores are filled with values coming from combat analyses (see below).

After that the AI modifies the scores 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 the AI leaders can recruit, the CA simulates fights against all enemy units on the map. If there are less then 5 enemy units, the enemy recruitment list(s) are also taken into account.

After combat analysis the score map may look like this:

Goblin Spearman score:		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

The combat analysis uses the function compare_unit_types(A, B). It takes two unit types and simulates a fight, using a cache because simulation is expensive. The function returns a positive value if unit type A is better than B and a negative value if unit type B is better than 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 snippet from compare_unit_types():

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.;
}

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.

The return value of compare_unit_types() is multiplied by the enemy's current hitpoints 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 can be quite inconvenient for a generalized treatment. They could all be negative, very high or too similar. Thus, the scores are transformed linearly. The resulting scores are numbers between 0.0 and 100.0. The minimum depends on the aspect recruitment_diversity.

Simplified code snippet:

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

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

Aspect recruitment_instructions

This is a powerful aspect for controlling recruitment via WML. Its syntax is quite complex and requires the composite aspect configuration. At its core are the [recruit] tags, which are the instructions to the candidate action defining which units are to be recruited.

[ai]
  [aspect]
    id=recruitment_instructions
    [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 numbers specifying the turns. '-' can be used between two values to define a range. An empty string means all turns.
  • The other keys possible in [facet] tags can, in principle, also be used, but note that invalidate_on_gamestate_change and invalidate_on_minor_gamestate_change have no effect on [recruit] tags. They are read at the beginning of the turn and are immutable for the remainder of the turn.

Attributes inside [recruit]:

  • id: (string) (Version 1.13.5 and later only) By specifying an ID it becomes possible to manipulate the [recruit] tags using modify_ai. This is entirely optional.
  • type="": (string) This key takes a comma separated list containing unit types, usages or levels. Common values for usage are 'scout', 'fighter', 'archer', 'healer' and 'mixed fighter'. An empty string means 'all units'. If more than one unit is specified, the AI decides what to recruit according to the score map.
  • number=-1: (integer) A number n greater than 0 instructs the AI to recruit n units. '-1' means 'as much as possible'. '0' means 'do not recruit'.
  • importance=1: (integer) Priority of the recruitment. The AI first recruits units with the highest importance, until the specified limits are reached. If gold is lacking or the castle is full, only the most important units are recruited, all other [recruit] jobs are dropped.
  • leader_id="": (string) id of the leader to execute this recruitment. Empty string means 'all leaders'. Note that this is only a recommendation to the AI. If the specified leader has no free hexes available, the AI uses other leaders to do the job.
  • total=no: (boolean) If set to 'yes', the AI counts the own units on the map which match at least one of the given types and then recruits the difference between number and the counted amount.
  • blocker=yes: (boolean) If set to 'yes', the AI stops recruiting if this job cannot be done (for example, because of lack of gold or a [limit]). If set to 'no', the AI drops this job and tries to continue with less important ones.
  • pattern=no: (boolean) If set to 'yes', the unit to recruit is not 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 recruits 6 units, with the probability that one unit is a grunt being twice as high as that of a scout. If the type attribute is empty, the AI recruits randomly. (See also recruitment_pattern)

Attributes inside [limit]:

  • id: (string) (Version 1.13.5 and later only) By specifying an ID it becomes possible to manipulate the [limit] tags using modify_ai. This is entirely optional.
  • type="": (string) This key takes a comma separated list containing unit types, usages or levels. An empty string means 'all units'.
  • max=0: (int) The maximum of units of the given type(s).

Notes:

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

The AI then falls back to normal recruiting when all other [recruit] tags are dealt with. This is also the content of the [default] facet for the aspect.

  • Even if only a [limit] tag is used in the aspect declaration, the [default] facet will get overwritten. Include the [recruit] tag mentioned in the previous point to let the AI recruit something. (Version 1.13.5 and later only) As of 1.13.5, if there is a [limit] tag but no [recruit] tag, an empty [recruit] tag is implied, allowing the AI to recruit. If both [limit] and [recruit] tags are present, no additional [recruit] tag is ever implied.
  • Only one facet at a time can be active. When there are more [facet]s defined in a turn, only one will be used (usually the first one). Use several [recruit] and/or [limit] tags in the same facet instead.
  • Do not define more than one [recruit] tag with the same importance. Also note that the aspect recruitment_pattern expands into a [recruit] tag with importance=1.
  • (Version 1.13.5 and later only) The tags [total] and [pattern] may be used instead of [recruit]. These have basically the same meaning and contents as [recruit], but imply either total=yes or pattern=yes. They are converted to [recruit] tags and can be manipulated as such by modify_ai.

Examples

Recruit 3 grunts (and nothing else) on turns 3-5.

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

Recruit as many scouts as possible until there are 4 scouts in total, 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 to the AI) and nothing else.

[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 'leader2'. Try to recruit the grunts even if 'leader1' cannot 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]

Always recruit random units.

[ai]
  [aspect]
    id=recruitment_instructions
    [facet]
      [value] 
        [recruit]
          type=
          pattern=yes
          importance=1
        [/recruit]
      [/value]
    [/facet]
  [/aspect]
[/ai]

Do not recruit on 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]
        [recruit]         #Do not forget to include the default
          importance=0    #[recruit]-tag. Otherwise the AI won't
        [/recruit]        #recruit anything.
      [/value]
    [/facet]
  [/aspect]
[/ai]

The following does not work because 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]

Instead, one has to use turns=1-2,4-10 in the first facet.

Aspect recruitment_save_gold

How it works

For several reasons it can be a good idea not to spend all gold at once:

  • To save upkeep
  • To wait for the enemy to recruit first and thus to counter more easily
  • To save gold for the next scenario when the enemy is almost defeated

It can also be a good idea (especially for the AI) to recruit in waves (instead of recruiting one unit or so each turn). If this type of behavior is desired, there is 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 for 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, where the costs are the sum of the cost of all units on the map weighted by their HP. The AI also attempts to estimate the total income over the next 5 turns.

If the AI is in state NORMAL and the unit_ratio exceeds a predefined threshold (defined in begin, see below) and the estimated income is positive, the AI switches to state SAVE_GOLD. Now the AI does not recruit units anymore until unit_ratio falls below another predefined threshold (defined in end). Then the AI switches back to state NORMAL and recruits again.

With this behavior the AI can save quite a lot of gold. If the gold then accumulates to more than another predefined value (spend_all_gold), it switches into state SPEND_ALL_GOLD and recruits a big army to start an offensive wave.

The AI ignores all those gold saving strategies when it is in state LEADER_IN_DANGER. This happens when an enemy is close to the AI leader (within 5 hexes).

Important note: While one might assume that the AI plays better when it tries to save some gold, tests show that this is generally not the case. They indicate that, for the current AI, it is always better for the AI to have as many units on the map as possible. This aspect is therefore deactivated by default.

The Aspect

This aspect also requires the composite syntax.

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

Attributes inside [value]:

  • active=0: (int) From this turn on the aspect is active. The AI always spends all gold until then. active=0 can be used to deactivate gold saving.
  • begin: (double) See explanation above.
  • end: (double) See explanation above.
  • spend_all_gold: (int) See explanation above. If set to '-1', the AI never switches to state SPEND_ALL_GOLD.
  • save_on_negative_income: (boolean) If this is set to yes, the AI switches to state SAVE_GOLD even if the income is negative (estimated over 5 turns, village gain and unit loss estimation included).

Example

[ai]
  [aspect]
    id=recruitment_save_gold
    [facet]
      [value]
        active=2
        begin=1.5
        end=1.1
        spend_all_gold=-1
        save_on_negative_income=no
      [/value]
    [/facet]
  [/aspect]
[/ai]

A macro can be used to deactivate recruitment_save_gold:

{AI_ASPECT recruitment_save_gold {AI_DEACTIVATE_SAVE_GOLD} }

Other Recruitment Related Aspects

  • recruitment_diversity=2.0: (double) When this value is high, the AI recruits more units which are currently rare on the map. When the value is 0.0, the AI only recruits the best unit. See above for implementation details.
  • 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 an easy hack for making the AI recruit more units of a specific type. (25 will be added to the unit type's score.) The same unit type can also be used several time. For example, with recruitment_more="Orcish Grunt, Orcish Grunt" it is possible to add a total of 50 to the Grunt's score.
  • recruitment_randomness=50: (int) A random value between 0 and recruitment_randomness is added to each score. Use a high value, such as '200', to increase randomness.
  • recruitment_pattern="": (string) This key takes a comma-separated list containing unit types, usages or levels. Common values for usage 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 expands into a recruitment_instructions facet with pattern=yes and importance=1. Make sure that there is no recruitment_instructions facet 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 set to '0', the AI does not recruit scouts to capture villages.

Making recruitment strong

The default values for the recruitment aspects are chosen to provide variety in the units the AI recruits. Tests have shown that it plays slightly better (against other AIs, at least) when it mostly recruits the units that score highest in the evaluation explained above. The best values found are:

  • recruitment_diversity=0.8: Let the AI recruit the best units only.
  • recruitment_randomness=0: Let the recruitment not be random.
  • villages_per_scout=0: The AI plays slightly better with fewer or no scouts.
  • deactivate recruitment_save_gold: The AI plays better when it spends all money.

In Multiplayer games, the AI 'Strong AI (RCA)' can be chosen with the settings as described here. (Version 1.13.5 and later only) As this is only a very minor modification of the default AI, this AI has been changed to a Dev AI (available in debug mode only) and has been renamed to 'Default AI (RCA) with Alternate Recruiting'.

Some notes on multiple leaders

The new recruitment CA is able to recruit with multiple leaders — even with different recruitment lists for each leader. However there are some drawbacks:

  • In the current version, the leaders recruit almost the same amount of units by default. This can partly be configured with the recruitment_instructions aspect and its attribute leader_id.
    • Note however that, as mentioned above, leader_id in recruitment_instructions is only a recommendation. For example, a leader recruits more units when enemies are near.
  • Multiple leaders with different recruitment lists in combination with a recruitment_pattern may not result in the expected behavior. For the AI it is more important that the leaders recruit the same amount of units than fulfilling the ratios given in recruitment_pattern.


See also: Wesnoth AI

This page was last edited on 27 February 2024, at 05:25.