Customizing AI in Wesnoth 1.8
From Wesnoth
This guide of AI customization was written for Wesnoth 1.8. Most of the information in it is still valid for Wesnoth 1.10, but it is not necessarily complete. For more up-to-date descriptions see AiWML and Practical Guide to Modifying AI Behavior.
Wesnoth 1.8 AI allows to customize the following things:
- AI turn sequence ( via [stage] tags )
- AI parameters ( via [aspect] tags )
- AI goals ( via [goal] tags )
- AI engines ( via [engine] tags )
Contents |
Using the new AI syntax
To use the new AI (RCA AI), either include AI configuration directly in [side], and/or write your own configuration in [side][ai].
Example: including new AI configuration directly in [side]
[side]
side=2
{ai/aliases/stable_singleplayer.cfg}
[/side]
Example: writing your own AI configuration from scratch
[side]
side=2
[ai]
{AI_NEW_SYNTAX}
#PLACE_YOUR_AI_CONFIGURATION_HERE
[/ai]
[/side]
Example: including new AI configuration directly in [side], and appending your AI configuration and/or modifications to it
[side]
side=2
{ai/aliases/stable_singleplayer.cfg}
[ai]
#PLACE_YOUR_AI_CONFIGURATION_HERE
[/ai]
[/side]
All those ways are equally supported.
Customizing AI turn sequence
AI turn sequence is a list of stages. if there are stages A,B,C, then the AI does stage A, then it does stage B, then it does stage C. If there are no stages, nothing will be done. Each stage can have an id. There are several types of stages:
- fallback to other ai (e.g., fallback to old 1.6 AI)
- candidate action evaluation loop, which is a base decision loop used by new RCA AI.
- a stage to execute a specific side-wide formula_ai formula.
- a stage to execute all unit formula_ai formulas.
- a stage to execute Lua AI code.
RCA AI is just one stage, main_loop, which is a candidate action evaluation loop.
To customize the AI turn sequence, you can modify the contents of that main_loop stage, add other stages alongside that main loop (generally, "do some things before that main loop"), or create a new turn sequence from scratch.
Working with main_loop of the RCA AI
main_loop stage of the new AI is a candidate action evaluation loop. This loop consists of a number of candidate actions. This decision loop works in the following way:
repeat {
score_list := evaluate_all_candidate_actions;
highest_score = max(score_list);
if (highest_score>0) {
execute_candidate_action_with_highest_score;
} else {
end_stage;
}
}
You can imagine 'candidate actions' as a group of people which gather in a single room and yell 'I want to play wesnoth!' (and the one who is loudest gets to play for as much as he wants - be it one action or the entire turn). Evaluation of the candidate actions should have no side effects - candidate action changes the game state only when is executed. If a candidate action is selected for execution but fails to change the game state (e.g. by trying to do an illegal move/attack/recruit), then it will be blacklisted until the end of turn. This is done to prevent infinite loops and the blacklisting is handled automatically by the engine. Let us see what candidate actions are in main_loop of the RCA AI.
[stage]
id=main_loop
name=testing_ai_default::candidate_action_evaluation_loop
{AI_CA_GOTO} # id="goto"
{AI_CA_RECRUITMENT} # id="recruitment"
{AI_CA_MOVE_LEADER_TO_GOALS} # id="move_leader_to_goals"
{AI_CA_MOVE_LEADER_TO_KEEP} # id="move_leader_to_keep"
{AI_CA_COMBAT} # id="combat"
{AI_CA_HEALING} # id="healing"
{AI_CA_VILLAGES} # id="villages"
{AI_CA_RETREAT} # id="retreat"
{AI_CA_MOVE_TO_TARGETS} # id="move_to_targets"
{AI_CA_PASSIVE_LEADER_SHARES_KEEP} # id="passive_leader_shares_keep"
[/stage]
As we can see, this is a [stage], which is processed by cpp engine (default engine). This stage has id main_loop, which can be used to reference this stage when adding/removing candidate actions. The order of candidate actions does not matter - the order of execution is dependent on the scores that they return. But, in this case, they are arranged, top to bottom, from highest-scoring to lowest-scoring candidate action. Those candidate actions have static priorities (i.e. each of them either returns 0 or a fixed value). The scores of those candidate actions are specified the following definitions:
#define AI_CA_GOTO_SCORE 200000 #enddef
#define AI_CA_RECRUITMENT_SCORE 180000 #enddef
#define AI_CA_MOVE_LEADER_TO_GOALS_SCORE 140000 #enddef
#define AI_CA_MOVE_LEADER_TO_KEEP_SCORE 120000 #enddef
#define AI_CA_COMBAT_SCORE 100000 #enddef
#define AI_CA_HEALING_SCORE 80000 #enddef
#define AI_CA_VILLAGES_SCORE 60000 #enddef
#define AI_CA_RETREAT_SCORE 40000 #enddef
#define AI_CA_MOVE_TO_TARGETS_SCORE 20000 #enddef
#define AI_CA_PASSIVE_LEADER_SHARES_KEEP_SCORE 10000 #enddef
These scores will stay the same through 1.8, so you can safely depend on this specific order of things. Let us return to main_loop
[stage]
id=main_loop
name=testing_ai_default::candidate_action_evaluation_loop
{AI_CA_GOTO} # id="goto"
{AI_CA_RECRUITMENT} # id="recruitment"
{AI_CA_MOVE_LEADER_TO_GOALS} # id="move_leader_to_goals"
{AI_CA_MOVE_LEADER_TO_KEEP} # id="move_leader_to_keep"
{AI_CA_COMBAT} # id="combat"
{AI_CA_HEALING} # id="healing"
{AI_CA_VILLAGES} # id="villages"
{AI_CA_RETREAT} # id="retreat"
{AI_CA_MOVE_TO_TARGETS} # id="move_to_targets"
{AI_CA_PASSIVE_LEADER_SHARES_KEEP} # id="passive_leader_shares_keep"
[/stage]
So, this main loop can be read as:
repeat {
IF it is possible to do a goto move, do it;
ELSE IF it is possible to recruit, do it;
ELSE IF it is possible to move leader to leader_goal, do it;
ELSE IF it is possible to return leader to keep, do it;
ELSE IF it is possible to make a good attack, do it;
ELSE IF it is possible to move a unit to heal, do it;
ELSE IF it is possible to move units to villages, do it;
ELSE IF it is possible to retreat, do it;
ELSE IF it is possible to move units to targets, do it;
ELSE IF it is possible for our leader to share the keep, do it;
ELSE end_stage;
}
To work with this stage, we can either add or delete candidate actions. To do this, there are macroses, {MODIFY_AI_ADD_CANDIDATE_ACTION SIDE STAGE_ID CANDIDATE_ACTION} and {MODIFY_AI_DELETE_CANDIDATE_ACTION SIDE STAGE_ID CANDIDATE_ACTION_ID}. Internally, these are simple macroses which use the [modify_ai] tag. Those macroses can be used either in [side][ai] or in [event] - so, it is possible to modify the AI at any moment of the game, from a WML event handler.
Example: making the AI of side 2, which does not recruit (by removing recruitment-related candidate actions)
[side]
side=2
{ai/aliases/stable_singleplayer.cfg}
[ai]
{MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop recruitment}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop move_leader_to_keep}
[/ai]
[/side]
Example: same things as above, but from an arbitrary [event] (this is recommended way to do things, since it's easier to test if it's in standalone event handler)
[side]
side=2
{ai/aliases/stable_singleplayer.cfg}
[/side]
[event]
name=ai_2_stop_recruiting
{MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop recruitment}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop move_leader_to_keep}
[/side]
Candidate actions can be written in c++, in formula_ai or in lua_ai. formula_ai and lua_ai-based candidate actions give us an easy way to add "IF this, THEN do that WITH PRIORITY score" behaviors to AI.
Example: make the ai retreat to specific location
[event]
name=ai_retreat_start
first_time_only="no"
# 'main_loop' is the id of the stage from data/ai/ais/testing_ai_default.cfg
{MODIFY_AI_ADD_CANDIDATE_ACTION 2 main_loop (
[candidate_action]
# this is the ID we refer when deleting this candidate action
# in the ai_retreat_stop event
id=retreat_to_map_edge
# we use fai engine to parse this config block
engine=fai
# this is fai-specific stuff - a name of the action.
# just set it to same thing as id
name=retreat_to_map_edge
# this is fai-specific stuff - a type of the action.
# in this case, this is a movement candidate action.
# all units which still can move, are considered.
type=movement
# this is "WHEN". the action is considered
# for execution if we return priority >0
# highest-priority action is taken
# Here we want just a bit less-priority thing than combat.
# Use {AI_CA_GOTO_SCORE}+10 to make the retreat happen first
# - even before gotos are considered.
# the intent of the formula is "if we can make a move closer to
# location, return good score, else return 0".
# it is important to return 0 if we cannot make a move.
evaluation="if(next_hop(me.loc,loc(1,1))=null,0,if(unit_at(next_hop(me.loc,loc(1,1)))=null, {AI_CA_COMBAT_SCORE}-10 ,0))"
# the above line is working around a bug, which'll be fixed in 1.7.12, after that the line below will work, too
# evaluation="if(unit_at(next_hop(me.loc,loc(1,1)))=null, {AI_CA_COMBAT_SCORE}-10 ,0)"
# this is "WHAT TO DO"
action="move(me.loc,next_hop(me.loc,loc(1,1)))"
[/candidate_action]
)}
[/event]
[event]
name=ai_retreat_stop
first_time_only="no"
# we reference the id of the above candidate action,
# which is part of 'main_loop' stage of the ai.
{MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop retreat_to_map_edge}
[/event]
Example: make the AI 'push to' the specific location, attacking units which bar its way.
[event]
name=ai_push_start
first_time_only="no"
{MODIFY_AI_ADD_CANDIDATE_ACTION 2 main_loop (
[candidate_action]
id=push_to_destination
engine=fai
name=push_to_destination
type=movement
# the intent of the formula is
# "let all units do the move before combat".
# we can use if() to limit the units who are affected.
evaluation="{AI_CA_COMBAT_SCORE}+10"
# the intent of the formula is
# "move from my current location to the location which is
# closest to my destination, ignoring enemy units" We'll
# end next to them if they block our way, and we'll fight
# in the combat phase normally.
action="move(me.loc, choose(unit_moves(me.loc),'mloc',-sum(map(simplest_path(mloc,loc(14,9),me.loc), 'path_location', movement_cost( me, path_location ) ))) )"
[/candidate_action]
)}
[/event]
Fallback stage
Fall back to other, older, AI
Example:
[stage]
id=fallback
name=testing_ai_default::fallback
[ai]
ai_algorithm=default_ai
[/ai]
[/stage]
Lua stage
Execute Lua code, call a function from the AI created inside a Lua engine. Whatever 1 element is returned by the code in lua [engine], is accessible here as an upvalue. So, for example, (...):execute() calls whatever_is_returned_by_lua_engine:execute()
Example:
[stage]
engine="lua"
code= "(...):execute()"
[/stage]
A stage to execute side formulas
This stage executes a given formula ai side formula.
Example:
[stage]
engine=fai
name=side_formulas
move="write_your_formula_here"
[/stage]
A stage to execute unit formulas
this stage takes no parameters and executes all formula ai formulas which are attached to units.
Example:
[stage]
engine=fai
name=unit_formulas
[/stage]
Tweaking AI parameters ( via [aspect] tags )
There is a number of ai parameters which can be considered more-or-less 'global', and which can be easily set up to be different on time-of-day\turn basic. Most of them are derived from AiWML parameters, and they can be set using helper macros like:
{AI_SIMPLE_ALWAYS_ASPECT recruitment_pattern "fighter,fighter,archer,healer"}
{AI_SIMPLE_ALWAYS_ASPECT_VALUE aggression "0.7"}
WARNING: WORK IN PROGRESS IN THIS SECTION
General Parameters
- aggression=0.4: (double) (value ranged from -infinity to 1.0 ) This key affects how an AI player will fight.
- In the following formulas of type X - Y = Z, X is always 1 and Z is the value that aggression should be set to: aggression=Z.
- It determines how an AI considers the difference between its units and its opponents by taking the value 1 - AI unit value in proportion to opponent unit value. ('Unit value' here means that the AI weights its decision on the chance to kill a unit or be killed, using a combination of its own units.) So, to make an AI which considers its units worthless, i.e. only cares about how much damage attacks inflict, set aggression at 1 - 0 = 1.0. This is the highest meaningful value for aggression; although it is insane, it is used on many HttT levels. If an AI set on this value can inflict 1 damage and take 0, or inflict 2 damage and take 20 himself, he'll take the latter option. To make an AI which considers its units equally as valuable as its opponent's, i.e. only attacks if he feels he can inflict more damage than he receives, set aggression at 1 - 1 = 0.0.
- Although an AI which considers its opponent's units worthless is impossible under this scheme, an AI which, for example, considers its units twice as valuable as its opponent's can be represented by aggression 1 - 2 = -1.0.
- The default is 1 - 1/2 = 0.5, which means the AI is content with dealing only half the damage to his opponent of that which he himself takes.
- The good value for aggression is equal to the ai relative gold advantage. e.g. if ai has 800 gold, and player has 200 gold, then 0.75 is a good value (then, the ai will be ok with inflicting 10 damage while taking 40), which will, roughtly, lead it to consider it ok to spend 800 gold to kill 200 gold of units. Do not set too low (say, lower than 0), because the AI doesn't know how to play defensively.
- attack_depth=5: (int) a number from 1 to 6. This is how many units will be thought about to attack a single unit. If the attack is completed, the AI on another round of thinking may still decide to attack that unit again with other units. Note that in battles, a unit usually can't be attacked from all six sides anyway, so any high attack_depth will be about the same. If attack_depth is low, the AI won't want to attack unwounded units very much, because it doesn't think it can kill them in the attack.
- avoid=[not][/not] (slf) strictly forbids the AI to move to specified set of locations.
Example:
{AI_SIMPLE_ALWAYS_ASPECT_VALUE avoid
(
[not]
x,y=20,16
radius=6
[/not]
)}
{MODIFY_AI_ADD_ASPECT 2 avoid (
[facet]
id="stay_in_own_land"
[value]
[not]
x=20-24,19-30
y=10,11
[/not]
[/value]
[/facet]
)}
- leader_aggression=-4.0 (double) exactly as aggression, but for units which can recruit.
- passive_leader=no: (bool) if 'yes' the AI leader will never move or attack, except to obey [leader_goal]s.
Removed parameters
- [protect_location]="": use [goal] name=protect_location instead
- [protect_unit]="": use [goal] name=protect_unit instead
- [target]="": use [goal] instead
Deprecated Recruiting Parameters
use them freely for, but I think we'll remove them in a future version once there's a new recruitment available
- number_of_possible_recruits_to_force_recruit=3.1: (double) a number higher than 0.0. Tells AI to force the leader to move to a keep if it has enough gold to recruit the given number of units. If set to 0.0, moving to recruit is disabled.
- recruitment_ignore_bad_movement=no: (bool) if 'yes', the AI will not analyse the terrain to see if the unit is suitable for it.
- recruitment_ignore_bad_combat=no: (bool) if 'yes', the AI will not analyse the units the enemy has fielded to see if the unit is suitable for fighting them.
- recruitment_pattern="": (string) This key takes a comma separated list containing the usages defined in the recruitable units. 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 AI considers all units with the specified usage(s) so make sure the units you want recruited are really covered by the pattern or use an empty pattern to have all available units considered regardless of usage. 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,archer" means, the AI will recruit twice as many fighters as archers, and not recruit scouts (other than scouts for capturing villages, who are recruited independently), healers or mixed fighters.
Deprecated Targeting Parameters
use them freely for now, but I think we'll remove them in a future version.
- protect_leader=2.0 and protect_leader_radius=10: the way these parameters work, is to target any enemy units that come within 'protect_leader_radius' of the AI leader with a value of 'protect_leader' on the units. Deprecated. Use this goal instead:
[goal]
name=protect_my_unit
[criteria]
canrecruit=yes
[/criteria]
value={YOUR_PROTECT_LEADER_VALUE}
protect_radius={YOUR_PROTECT_LEADER_RADIUS}
[/goal]
- simple_targeting=no: (bool) When the AI decides what targets to move towards with which units, it considers all its units that can move, sequentially. For each unit, it decides what the best target for it to go towards is. If not 'yes', it will check if any other units would be better to go towards that target than the unit it is currently considering. Better use a simpler movement and targeting candidate action.
- support_villages=no: (bool) Trigger a code path that will try to protect those villages which are threatened by the enemy. This seems to cause the AI to just 'sit around' a lot, so it's only turned on if it's explicitly enabled. don't use
- village_value=1: (double) A number 0 or higher which determines how much the AI tries to capture villages. Deprecated. Better set to 0 and use
[goal]
[criteria]
terrain=*^V*
[/criteria]
value={YOUR_VILLAGE_VALUE}
[/goal]
AI goals ( via [goal] tags )
AI goals are used to supply additional information about the map. Internally, the various goals are translated to markers (with associated values) which are placed on map locations. The values of markers in the same location are added together, and this sum is used by certain parts of the AI (in particular, by the move-to-targets phase and the grouping code) – the bigger the value, the more resources devoted to the goal. Not all parts of the AI use these goals, though (in particular, the selection of attack targets does not, which is significant since the combat phase has a higher priority than the move-to-targets phase).
There are five goals available at the moment: target goal (the default), target_location goal (This feature was implemented in 1.9) and protect_unit, protect_my_unit, and protect_location goals.
target [goal]
The (default) target goal specifies target units towards which the AI should move its units.
[goal]
[criteria]
#SUF HERE
[/criteria]
value={VALUE}
[/goal]
- [criteria] Contains a SUF describing the target units (not necessarily enemy units).
- value=0: (int) The value of the goal.
Example: When converting the old [target] tag to the new [goal] tag,
[target]
value=5
side=3
[/target]
becomes
[goal]
[criteria]
side=3
[/criteria]
value=5
[/goal]
target_location [goal]
The target_location goal specifies target locations towards which the AI should move its units.
[goal]
name=target_location
[criteria]
#SLF HERE
[/criteria]
value={VALUE}
[/goal]
- [criteria] Contains a SLF describing the target locations.
- value=0: (int) The value of the goal.
protect_location [goal]
The protect_location goal specifies locations that the AI should protect. Enemy units within the specified distance (radius) of one of these locations are marked as targets with the provided value.
[goal]
name=protect_location
[criteria]
#SLF HERE
[/criteria]
value={VALUE}
protect_radius={PROTECT_RADIUS}
[/goal]
- [criteria] Contains a SLF describing the target locations.
- value=0: (int) The value of the goal.
- protect_radius=20: (int) The protection radius.
Example: When converting the old [protect_location] tag to the new [goal] tag,
[protect_location]
value=5
x,y=42,20
radius=16
[/protect_location]
becomes
[goal]
name=protect_location
value=5
protect_radius=16
[criteria] #NOTE: this is a SLF, because we're protecting a location
x,y=42,20
[/criteria]
[/goal]
protect_unit [goal]
The protect_unit goal specifies units (of all sides!) that the AI should "protect". Enemy units within the specified distance (radius) of one of these units are marked as targets with the provided value.
[goal]
name=protect_unit
[criteria]
#SUF HERE
[/criteria]
value={VALUE}
protect_radius={PROTECT_RADIUS}
[/goal]
- [criteria] Contains a SUF describing the units to "protect".
- value=0: (int) The value of the goal.
- protect_radius=20: (int) The protection radius.
Example: When converting the old [protect_unit] tag to the new [goal] tag,
[protect_unit]
value=5
side=3
radius=16
[/protect_unit]
becomes
[goal]
name=protect_unit
value=5
protect_radius=16
[criteria] #NOTE: this is a SUF, because we're protecting a unit
side=3
[/criteria]
[/goal]
protect_my_unit [goal]
The protect_my_unit goal specifies units from the AI's own side that the AI should protect. (This is basically the protect_unit goal with an implied side= in the filter, restricting matching units to the AI's side.) Enemy units within the specified distance (radius) of one of these units are marked as targets with the provided value.
[goal]
name=protect_my_unit
[criteria]
#SUF HERE
[/criteria]
value={VALUE}
protect_radius={PROTECT_RADIUS}
[/goal]
- [criteria] Contains a SUF describing the units to protect.
- value=0: (int) The value of the goal.
- protect_radius=20: (int) The protection radius.
Example: When converting the old protect_leader attribute to the new [goal] tag,
protect_leader=10 protect_leader_radius=9
becomes
[goal]
name=protect_my_unit
value=10
protect_radius=9
[criteria] #NOTE: this is a SUF, because we're protecting a unit
canrecruit="yes"
[/criteria]
[/goal]
To modify goals from WML events, use these helper macros (don't forget to give your [goal]s unique ids):
{MODIFY_AI_ADD_GOAL SIDE GOAL_CONFIG}
{MODIFY_AI_DELETE_GOAL SIDE GOAL_ID}
{MODIFY_AI_TRY_DELETE_GOAL SIDE GOAL_ID}
AI engines ( via [engine] tags )
Engines are, generally, created automatically. There's no need to use [engine] tags by hand with c++ or fai engines, they are always available, but the [engine] tags can be used to set the initial state of the engine (e.g. initialize formula ai variables). By contrast, the lua engine always needs to be defined specifically.
Example: lua engine
[engine]
name="lua"
code= <<
--! ==============================================================
local ai = ...
local my_ai = { }
local ai_stdlib = wesnoth.require('ai/lua/stdlib.lua');
ai_stdlib.init(ai)
function my_ai:example()
ai.say_hello()
self:do_moves()
end
function my_ai:do_moves()
my_leader = wesnoth.get_units({canrecruit = true, side = ai.side})[1]
--! move (partial move)
ai.move(my_leader,13, 22)
--! full move. note that the my_leader still can be used although x and y are now different.
ai.move_full(my_leader, 11, 23)
--! attack with auto weapon/aggression
ai.attack(2, 12, 3, 12)
--! attack with weapon selected
ai.attack(3, 11, 3, 12, 1)
--! attack with different aggression
ai.attack(3, 13, 3, 12, -1, 0.9)
if wesnoth.current.turn==1 then
--! recruit on specific location
ai.recruit("Vampire Bat", 10, 22)
--! recruit on any suitable location
ai.recruit("Skeleton")
--! recall on any suitable location.
ai.recall("Kiressh")
end
end
return my_ai
--! ==============================================================
>>
[/engine]
Formula AI syntax
See FormulaAI and FormulaAI_Functions
Lua AI syntax
To use lua AI, you need to set up a lua AI engine. In this engine, you should take a lua table which contains c++ lua ai support functions, and add 'standard library' of Lua AI functions to it, and use the resulting object in your Lua AI functions. (Note that 'stdlib.lua' does not currently contain any functionality, so it does not need to be included at this time. This might, however, change in a future version.)
[engine]
name="lua"
code= <<
local ai = ...
local ai_stdlib = wesnoth.require('ai/lua/stdlib.lua');
ai_stdlib.init(ai)
my_ai = {}
!-- set up functions of my_ai here
return my_ai
>>
[/engine]
when writing functions of my_ai, you have access to ai table, which contains the following functions
- ai.side side number of the AI
- ai.move(FROM_X,FROM_Y,TO_X,TO_Y) partial move from FROM_X,FROM_Y to TO_X,TO_Y. Each location can be given either by unit object or as a pair of comma-separated coordinates.
- ai.move_full((FROM_X,FROM_Y,TO_X_TO_Y) full move from FROM_X,FROM_Y to TO_X,TO_Y. Each location can be given either by unit object or as a pair of comma-separated coordinates.
- ai.attack(ATTACKER_X,ATTACKER_Y,DEFENDER_X,DEFENDER_Y) attack from ATTACKER_X,ATTACKER_Y to DEFENDER_X,DEFENDER_Y. Each location can be given either by unit object or as a pair of comma-separated coordinates. Best weapon is selected. Default aggression is used.
- ai.attack(ATTACKER_X,ATTACKER_Y,DEFENDER_X,DEFENDER_Y,WEAPON) attack from ATTACKER_X,ATTACKER_Y to DEFENDER_X,DEFENDER_Y. Each location can be given either by unit object or as a pair of comma-separated coordinates. WEAPON weapon is used (0-based). WEAPON -1 means 'select best weapon'. Default aggression is used.
- ai.attack(ATTACKER_X,ATTACKER_Y,DEFENDER_X,DEFENDER_Y,-1,AGGRESSION) attack from ATTACKER_X,ATTACKER_Y to DEFENDER_X,DEFENDER_Y. Each location can be given either by unit object or as a pair of comma-separated coordinates. weapon -1 means 'select best weapon'. Aggression AGGRESSION is used.
- ai.recruit("UNIT NAME", RECRUIT_X,RECRUIT_Y) recruit a unit by name. Optional recruit location can be given either by unit object or as a pair of comma-separated coordinates.
- ai.recruit("UNIT NAME") recruit a unit by name on any suitable location.
- ai.recall("UNIT ID", RECALL_X,RECALL_Y) recall a unit by id. Optional recall location can be given either by unit object or as a pair of comma-separated coordinates.
- ai.recall("UNIT ID") recall a unit by id on any suitable location.
[modify_ai] tag
[modify_ai] is a powerful WML tag which allows to add and delete ai components. it can be used either in [side][ai] and in [event]. [modify_ai] in [side][ai] is activated when ai is first initialized (it's guaranteed to happen before the ai acts, but, otherwise, it can happen at any moment, for example, when game is saved) So, for example, you might see the uninitialized version of the config (with unapplied [modify_ai] tags) when using :inspect .
To debug modify_ai tags, use --log-debug=ai/mod
- action
- add: add a component to the specified path
- delete: delete the specified path
- try_delete: delete if exists. doesn't throw warnings if modify_ai fails to change the ai.
- path: path to component
- aspect[id_of_aspect] aspect with specific id (cannot be deleted or changed, id must be well-known)
- aspect[id_of_aspect].facet[id_of_facet]' facet with specific id of aspect of specific id.
- stage[id_of_stage] stage with specific id
- stage[0] stage 0 (first)
- stage[1] stage 1
- stage[*] all stages
- stage[id_of_candidate_action_evaluation_loop_stage].candidate_action[id_of_candidate_action] candidate action with specific id of stage with specific id.
- stage[id_of_candidate_action_evaluation_loop_stage].candidate_action[*] all candidate actions of stage with specific id.
- goal[id_of_goal] goal with specific id
- goal[0] goal 0 (first)
- goal[1] goal 1
- goal[*] all goals
- side: side whose ai is to be modified, only needed if [modify_ai] is used inside [event]. note: Default side=1 for empty side= is deprecated.
There's a large number of helper macroses for AI modifications. see [1]
how AI works, a short developer-oriented overview
(taken from irc log)
Firstly, we need to get the ai configuration from WML. It is either already in [side][ai], or is injected there (e.g., depending on the ai that user selects on the game creation screen). In either case, just after we create team, we get that config and start 'parsing' it. see src/team.cpp, team_info::team_info and manager::add_ai_for_side_from_config
We store that parsed&prepared config, and, on first access to the ai (for example, on the start of first ai turn) we 'initialize' it, creating C++ objects from that config. See holder::init in src/ai/manager.cpp - see it, it's important. Basically, we create a group of 'support objects', one by one. Each 'next' support object contains previous one. Those 'support objects' are called 'contexts', since all ai code is 'in context' of those objects. They are in src/ai/contexts.cpp, src/ai/default/contexts.cpp, src/ai/composite/context.cpp
side_context just holds the side number. readonly_context holds all the 'basic information' that the ai has about the game state. See src/ai/game_info.hpp for a list - map, units, teams, time of day, etc. Also readonly_context contains derived information such as movement maps (who can move where - that info is calculated from the 'basic' info) and two classes of ai components - aspects and goals. 'aspects' are containers for well-known ai parameters like aggression, avoided locations, caution, recruitment_pattern. goals are c++ representations of things like 'protect this location' and 'target those units' (they are used in movement and targeting phase of the turn to determine where to move units). To parse those components, we firstly find a suitable 'factory' which will parse a config into a c++ object. Those factories are called 'engines' and their sole purpose is to allow ai components such as goals and aspects to be written in different languages, such as c++, formula ai, and lua. See src/ai/composite/engine* for them. So, if we got a component config, we 'peek' into it, find the engine= part, and if it's not set or equals to "cpp", we pass that config to c++ engine, else if it has engine=fai, we pass that config to formula ai engine, else if it has engine=lua, we pass it to lua engine. On receiving the config, engine does its 'factory' work and returns a c++ wrapper which 'behaves like a goal' or 'behaves like an aspect' - so, the rest of the code can use it without worrying for the language in which it is implemented. So, to repeat, readonly_context contains derived information such as movement maps and two classes of ai components - aspects and goals. it creates the necessary engines to create those components. Then, readwrite_context contains functions to do ai actions (which are defined in src/ai/actions.?pp) - movement/attack/recall/recruit routines which actually add things to replay, change the game state. Then, default_ai_context contains some helper functions which are used by the ai, like count_free_hexes_in_castle, find_targets, rate_terrain. generally, it is the leftovers of old default ai and will gradually be moved elsewhere. Then, the ai_composite is created. see src/ai/composite/ai.cpp - ai_composite is AI whose turn sequence is a collection of parts called stages. So, it contains a new type of ai component, called 'stage'. it also uses engines to create them, so stages can be written in c++, formula_ai, or lua. A stage is just C++ object which represents a part of AI turn, with a single method do_play_stage() which plays it. a stage is in context of ai_composite, which is in context of default_ai_context, which is in context of readwrite_context which is in context of readonly_context which is in context of side_context. So, if you, say, write a C++ stage, you have access to all the info in all those contexts. So, you can get info about the game state, do moves/attack/etc. You can write an entire ai using stages. But, stage-based ai has a problem - if you have, say stages 'recruit' and 'move', you do all the recruits first, and then all the moves first. but, if your keep is full, you do no recruits and then do moves, but don't return to recruit stage this turn (as it's over). So, to solve that 'stages are not enough' problem, we have one stage which is 'special', it is called 'candidate action evaluation loop stage' and contains components called 'candidate actions' (those, too, are parsed by ai engines, so they can be written in c++/formula ai/lua ) it is in src/ai/testing/stage_rca.?pp - it does a turn slightly differently - see http://wiki.wesnoth.org/Customizing_AI_in_Wesnoth_1.8#Working_with_main_loop_of_the_RCA_AI (read the text description of candidate action evaluation loop in there). So, to code an ai, we code a group of components such as aspects, goals, stages, and candidate actions. There is an API to add/remove those components from the ai 'on the fly', via [modify_ai] wml tag. Current trunk AI is implemented as a group of candidate actions, most of them is in src/ai/testing/ca.cpp
See Also
This guide of AI customization was written for Wesnoth 1.8. Most of the information in it is still valid for Wesnoth 1.10, but it is not necessarily complete. For more up-to-date descriptions see
