Creating Custom AIs

From The Battle for Wesnoth Wiki

Too Many Methods for Creating Custom AIs

The methods available for creating custom AIs have been developed over many development cycles. As a result, it is now much easier to create custom AIs than it used to be. The down side is that there are now a large number of different methods to do more or less the same thing, which is unnecessary and, quite frankly, confusing. Also, while most of these methods still work for backward compatibility reasons, many of them are not maintained any more.

This page therefore only describes one method of creating custom AIs, custom Lua candidate actions. Links to legacy documentation of some of the old methods are still included at the bottom of this page, but just as the methods themselves, these wiki pages are not maintained and updated any more. We recommend not using them any more and, ideally, update old code to the new methods.

Note: The current type of custom Lua candidate actions was originally called external Lua CAs, to distinguish them from the original method (now the old method) of setting up Lua CAs. However, as external Lua CAs are now the standard for creating custom AIs, we drop the 'external' in the description. It is mentioned here only in case you come across the term somewhere in older documentation.

Different Types of AI Modifications

Modifications of the AI behavior can be accomplished in a variety of different ways:

  • Adding new candidate actions
    • This is the main content of this page.
  • Changing the existing candidate actions:
    • Modifying the evaluation scores of the candidate actions
    • Deleting candidate actions
    • Instructions for these tasks are also given here.
  • Configuring the parameters/weights which determine how the default AI evaluates its moves.
    • This can be done easily in WML by setting aspects and goals.
    • It is also possible to assign dynamic aspects using Lua.
    • Technically this is not really creating custom AIs, but rather customizing the existing AI. It is not described on this page. See the links provided instead.

Setting Up a Custom AI

Creating Custom Candidate Actions

The recommended way of creating a custom AI is to add Lua candidate actions (CAs) to the main_loop stage of the default AI. Note that, in the following, we assume a solid knowledge of how the evaluation loop of this stage works, what candidate actions are in general and how they are used in the default AI, as well as some basic knowledge of Lua in Wesnoth and Lua AI.

We demonstrate how this is done on the example of the Return Guardian Micro AI. This is a very simple AI that is a variation of the default Wesnoth guardian. It has a defined guard position and only two states:

  • When the guardian is not on the guard position, it tries to return there.
  • When it is on the guard position, it is left free to attack enemy units that are within range, but may not do any other moves.

Note that some minor changes have been made to the code of the actual Return Guardian Micro AI for ease of presentation.

The code for the AI is as follows (we assume that this is in a file called 'my_addon/lua/return_guardian.lua'):

local AH = wesnoth.require "ai/lua/ai_helper.lua"

local function get_guardian(cfg)
    local filter = cfg.filter or { id = cfg.id }
    local guardian = AH.get_units_with_moves {
        side = wesnoth.current.side,
        { "and", filter }
    }[1]

    return guardian
end

local ca_return_guardian = {}

function ca_return_guardian:evaluation(ai, cfg)
    local guardian = get_guardian(cfg)
    if guardian then
        if (guardian.x == cfg.return_x) and (guardian.y == cfg.return_y) then
            return 99990
        else
            return 100010
        end
    end

    return 0
end

function ca_return_guardian:execution(ai, cfg)
    local guardian = get_guardian(cfg)

    -- In case the return hex is occupied:
    local x, y = cfg.return_x, cfg.return_y
    if (guardian.x ~= x) or (guardian.y ~= y) then
        x, y = wesnoth.find_vacant_tile(x, y, guardian)
    end

    local next_hop = AH.next_hop(guardian, x, y)
    if (not next_hop) then next_hop = { guardian.x, guardian.y } end

   if ((next_hop[1] ~= guardian.x) or (next_hop[2] ~= guardian.y)) then
       ai.move_full(guardian, next_hop[1], next_hop[2])
   else
       ai.stopunit_moves(guardian)
   end
end

return ca_return_guardian

The first 12 lines are not directly relevant to setting up the AI, they simply define some utility functions. The first important line is L.13 in combination with lines 15, 28 and 47. A Lua candidate action needs to return a table with an evaluation and an execution function. The mentioned lines set up that table and the two functions. Its name is chosen to be ca_return_guardian, but that is really irrelevant as it is a variable local to the file.

The execution and evaluation function can have several arguments. The available arguments and the order in which they appear depend on the Wesnoth version and usage.

Before Version 1.13.5

  • ai: The Lua AI function table. The execution function always needs this argument to be passed, the evaluation function only in some situations. See below for details.
  • cfg: A comma-separated list of configuration parameters, which are passed from the [candidate_action] tag using the eval_parms and exec_parms keys. These need to be in Lua table format, but without the outer curly brackets { }. See below for examples.
  • self: If the persistent variable data is needed, the evaluation/execution function definitions need to include a third parameter self. The persistent variable is then accessible as self.data. See below for more information on the data variable.

These arguments must appear in order (ai, cfg, self) in the function calls, but as always in Lua, trailing unused arguments can be omitted.

Starting from Version 1.13.5

The arguments passed to the functions are different for candidate actions using the recommended [args] subtag rather than eval_parms and exec_parms (these are explained in more detail below). The new arguments are as follows:

  • self: The engine data. By default, this replicates the self parameter from the old system for compatibility, meaning that self.data is the same as data. If an [engine] tag was explicitly defined for Lua, however, this parameter is the value that the engine's code returned, which could be pretty much anything.
  • cfg: The contents of the [args] subtag; parameters specific to this candidate action. With this, you can use the same code with different parameters to produce two different candidate actions.
  • data: The persistent variable data. This is the same as self.data in the old system. If an [engine] tag was explicitly defined for the Lua engine, this is initially equal to the value of the [data] subtag. However, unlike the other two parameters, this value is mutable.
  • filter_own: (Version 1.15.3 and later only) The content of the [candidate action][filter_own] tag as described here.

If the functions are defined using colon notation as in the above example, then the self parameter is implicitly defined and the arguments must be in order (cfg, data, filter_own) in the function calls. If you prefer to explicitly define it, use a dot instead of a colon when defining the function, in which case the arguments must be passed as (self, cfg, data, filter_own). As always in Lua, you can omit any of the arguments if the function does not need to access the respective information.

Notice that the ai table is no longer included among the arguments passed to the table. This is because it's now available as a "scoped global variable" anywhere in AI code, so there is no need to pass it around.

Evaluation Function

In our example, the call to the evaluation contains the first two of these arguments, the ai table and the configuration table cfg. The ai table is not needed for this particular evaluation function. It is there only as a placeholder because the cfg table is needed and always needs to be passed as the second argument.

The cfg table contains information about the guardian unit and the guard position. We will show how this is passed to the evaluation function later. For now, we only need to know that it contains either a filter or id key, used in L.16 (call to function get_guardian defined in L.4) to check whether there is a guardian unit on the map, and keys return_x and return_y defining the guard position.

A CA evaluation function returns a numerical score which determines when in the main_loop stage evaluation loop it is executed. The return guardian code evaluation has three possible return values. First (or rather: last), if no guardian unit is found it returns 0 (L.25), which means nothing is to be done for this CA.

If a guardian unit is found, there are two possible scores. The code first checks whether the guardian is at its guard position (L.18). If so, the return score is 99,990, if not, it is 100,010. The relevance of these values is that they are just below and just above the score of the combat (attack) CA of the default AI. Thus, if the guardian is not at its return position, the return guardian CA has a higher score than any attacks done by the default AI, and the unit is returned to its guard position whether there is an enemy unit in range or not.

If, on the other hand, the guardian is already at its guard position, the return score is lower than that of the default combat CA. The default AI is thus free to use the unit in an attack.

It is important to notice that the function may not return 0 when the guardian is at its guard position. Imagine the situation that the guardian is at its position and has no unit to attack. If the return score were 0 in this case, CAs with score lower than the combat CA would take over. In particular, the move-to-targets CA would kick in and move the guardian toward one of the default AI goals. This could be done by removing these other CAs, but then it would not be available for other, non-guardian units of the AI side either. A better way to deal with this is to remove the movement of the guardian unit if it is at its guard position and no attack happened. This is done by using a return score of 99,990 in this situation, in combination with what the execution function does, as described in the following.

Execution Function

The execution function is called with the same two arguments as the evaluation function. The cfg table is needed again in order to find the guardian (L.29) and the position to move to. The position is either the return hex (L.32) or, in case it is occupied by a unit other than the guardian itself, a vacant hex close to it (L.34). Lines 37 and 38 then find the closest hex the unit can move to in practice, using the ai_helper.lua utility function next_hop().

Lines 40–44 contain the actual execution of the AI move. If the guardian is not at the next_hop position, it will be moved there and movement will be set to zero; that's what ai.move_full() does, as opposed to ai.move(). If, on the other hand, the next_hop position is its current position (which means that it is either already at the guard position or cannot get any closer), movement is removed using ai.stopunit_moves(), so that the other CAs of the default AI do not move it away afterward. Note that, in the latter case, we do not remove attacks, so that the default AI is still free to attack if the guardian ends up next to an enemy unit.

This is it for the AI code itself. We can see that it is possible to set up reasonable versatile AI behavior with just a few dozen lines of code by working with and around the behavior of the default AI. In the following, we show how to add this code to the default AI. But first:

A few more notes:

  • It is very important that CA evaluation functions do not change the game state as that leads to out of sync (OOS) errors.
  • Also very important: 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. This means, however, that you need to be make sure that the execution of a move is possible, otherwise all subsequent moves which the CA would have done during this turn will not happen. This sort of check can, for example, be done with the ai.check*() Lua AI functions.
    • For a description of these functions, and all other functions of the ai table, see here.
  • The table returned by the code (ca_return_guardian in our example) may also contain other functions. For example, the get_guardian() function could be included in the table rather than by closure as we did it here. It does not matter what other elements the table contains, as long as the evaluation and execution functions exist.
  • For many more examples of other candidate actions, check out the Micro AI CAs] in the Wesnoth repository.

Including Custom Candidate Actions in Scenarios

The last step needed is adding this CA code into the AI configuration. The following instruction are for adding the AI to a scenario (this works in both single and multi player) at scenario setup time. Custom AIs can also be added to eras and modifications or while a game is in progress in very similar ways.

Let us assume that the code above is saved in a file called my_addon/lua/return_guardian.lua and that we want to activate it for a unit with id 'Bob' of side 1 with guard position (10,15). The easiest way of inserting the candidate action is by using the MODIFY_AI_ADD_CANDIDATE_ACTION macro.

[ai]
    {MODIFY_AI_ADD_CANDIDATE_ACTION 1 main_loop (
        [candidate_action]
            engine=lua
            name=return_guardian_bob
            id=return_guardian_bob
            max_score=100010
            location="~add-ons/my_addon/lua/return_guardian.lua"
            [args]
                id=Bob
                return_x=10
                return_y=15
            [/args]
            # In the older syntax, the above would have looked like this:
            # eval_parms="id = 'Bob', return_x = 10, return_y = 15"
            # exec_parms="id = 'Bob', return_x = 10, return_y = 15"
        [/candidate_action]
    )}
[/ai]

This needs to be placed in the [side] tag of side 1. Of course, you can also use the [modfiy_ai] tag directly without going through the macro. See here for the syntax.

Explanation of the keys:

  • engine: The engine to be used for this CA. Possible values are 'cpp', 'lua' and 'fai', but for a Lua AI CA it always has to be 'lua'.
  • id and name: These are, in principle, optional. However, they are generally useful to assign for clarity. Furthermore, if you want to remove or change a candidate action later, you need to know its id.
  • max_score: The maximum score the CA evaluation function might return. This parameter is also optional, but it is useful to reduce the evaluation time of the main_loop stage, which only evaluates CAs with max_score larger than those of the currently highest-scoring CA found. See here for a little more on that.
  • location: File name, including path, of the AI code for the CA
  • eval_parms, exec_parms: (string) The parameters to be passed to the evaluation and execution functions of the AI. These need to be strings in form of a Lua table, without the outer curly brackets. They are passed to the functions as the second argument, called cfg in the example above.
    • The strings do not necessarily need to be in WML object Lua table format. However, this is, of course, necessary if they are to be used in functions that require that format, as in the filter example shown below.
    • (Version 1.13.5 and later only) These string parameters are now replaced by the [args] WML tag. For the time being, the string parameters will continue to work, but they will be removed entirely in the next development cycle.
  • [args]: (Version 1.13.5 and later only) WML data to be passed to both the evaluation and execution functions. A copy of the data is passed, so even if the functions modify it, it will always be the same on the next call. This replaces eval_parms and exec_parms.

The example above requires

cfg = { id = 'Bob', return_x = 10, return_y = 15 }

to be passed to the evaluation and execution functions. This is used in the [candidate_action] tag code snippet shown above — without the out { }, as mentioned.

Alternatively, if you want to pass, for example, a [filter] tag that includes the unit id and type, it would look like this

cfg={ { filter, { id = 'Bob', type = 'Assassin' } }, return_x = 10, return_y = 15" }

resulting in the following parameter string definition

eval_parms="{ filter, { id = 'Bob', type = 'Assassin' } }, return_x = 10, return_y = 15"

Again, identical except for the outer curly brackets, and repeated for clarity.

That's most of what one needs to know for the creation of custom AIs. Some additional information on variations of using custom CAs as well as other AI modifications is given in the following. We also list some of the other methods available for adding custom AIs at the bottom of this page. As we point out above, however, we do not recommend using those any more. Neither the code itself nor the wiki pages are maintained any more at this time and, in an attempt to simplify and standardize the AI code, they might be removed at some point in the future.

The Persistent data Variable

It is sometimes necessary to transfer information between the evaluation and execution functions of the same CA or between CAs. It might also be necessary to have this information persist from turn to turn and across save/load cycles. The Wesnoth AI therefore adds a Lua table called data which is added to the third argument, self, of the evaluation and execution function declarations described above.

Some more information on its implementation in Lua AI is given here. For most practical intents and purposes it is sufficient to know that you can write at will to this table and read the information again from other functions and/or at other turns. The only things to keep in mind are:

  • The functions requiring access to the table need to have the third parameter specified in their declaration. If it is called self, the table is accessible as self.data.
  • If you want content to persist over save/load cycles, all content of data needs to be in WML table format. Other format content can also be used during an AI turn, for example to pass it between the evaluation and execution functions of the same CA, but it is lost when saving the game.
  • The CAs of the Micro AIs make extensive use of the data variable. You can check out their code for examples.

Custom AIs in Eras and Modifications

Custom AIs can also be included in eras and modifications for use with any MP map that allows them. The setup is entirely equivalent to that for scenarios, with two very minor differences:

  • The MODIFY_AI_ADD_CANDIDATE_ACTION macro needs to be placed inside [era][ai] or [modification][ai] instead of in [side][ai].
  • The description attribute needs to be set inside the [ai] tag. This is the text which will show up in the computer player selection menu during the MP game setup.
  • (Version 1.13.5 and later only) Scenarios wishing to use such AIs may reference them using the ai_algorithm key in [side][ai], and modify them further with additional AI component tags such as aspect values.

As an example, the following code adds the CA described above to a modification.

[modification]
    id=guardian_bob
    name= _ "Guardian Bob AI"
    description= _ "Adds code for Guardian Bob to the default AI"
    [ai]
        description=_"Multiplayer_AI^Default AI + Guardian Bob"
        {MODIFY_AI_ADD_CANDIDATE_ACTION 1 main_loop (
            [candidate_action]
                engine=lua
                name=return_guardian_bob
                id=return_guardian_bob
                max_score=100010
                location="~add-ons/my_addon/lua/return_guardian.lua"
                eval_parms="id = 'Bob', return_x = 10, return_y = 15"
                exec_parms="id = 'Bob', return_x = 10, return_y = 15"
            [/candidate_action]
        )}
    [/ai]
[/modification]

Mid-scenario Candidate Action Changes

The instructions given above are written for setting up custom candidate actions at the beginning of a game. However, the [modify_ai] tag and the macros using it can be used at any time during a scenario in an [event] tag. It is entirely equivalent to the methods described above.

Notes:

  • In this case, the [modify_ai] tag is placed directly inside the event, without requiring an [ai] tag.
  • See here for details and examples.

Additional Information for Creating Custom AIs

Adding the main_loop Stage from Scratch

Sometimes it might be useful or convenient to add the entire main_loop stage from scratch, rather than adding or deleting individual candidate actions. If that is desired, the syntax for the example of the default AI plus Guardian Bob from above looks as shown below.

(Version 1.15.3 and later only) The example below is from a previous Wesnoth version. New candidate actions have since been added to the default AI. The latest up-to-date version can always be found in file ai/ais/ai_default_rca.cfg in the Wesnoth data directory

[ai]
    description=_"Multiplayer_AI^Default AI + Guardian Bob"
    version=10710
    [stage]
        id=main_loop
        name=ai_default_rca::candidate_action_evaluation_loop
        {AI_CA_GOTO}
        {AI_CA_RECRUITMENT}
        {AI_CA_MOVE_LEADER_TO_GOALS}
        {AI_CA_MOVE_LEADER_TO_KEEP}
        {AI_CA_HIGH_XP_ATTACK} # Wesnoth 1.13.6 and later only
        {AI_CA_COMBAT}
        {AI_CA_HEALING}
        {AI_CA_VILLAGES}
        {AI_CA_RETREAT}
        {AI_CA_MOVE_TO_TARGETS}
        {AI_CA_LEADER_SHARES_KEEP}
        [candidate_action]
            engine=lua
            name=return_guardian_bob
            id=return_guardian_bob
            max_score=100010
            location="~add-ons/my_addon/lua/return_guardian.lua"
            eval_parms="id = 'Bob', return_x = 10, return_y = 15"
            exec_parms="id = 'Bob', return_x = 10, return_y = 15"
        [/candidate_action]
    [/stage]
[/ai]

This can be included in [side], [era] or [modification] tags.

Notes:

  • The description attribute is ignored if this is included in a [side] tag. It can be omitted in that case.
  • It used to be necessary to include even the unmodified RCA AI for computer controlled sides. Usually this was done by including a file with the AI configuration, for example by adding a line such as
 {ai/ais/ai_default_rca.cfg}

to the [side] tag. While it does no harm to still do so, it is not needed any more. If no other stage is defined, the default AI is automatically included for all computer controlled sides.

Modifying Default AI Candidate Actions

In addition to customizing the default AI behavior using aspects and goals, the default candidate actions can be deleted selectively and their scores can be changed.

Deleting Default AI Candidate Actions

Just as all the previous tasks described on this page, this can also be done with the [modify_ai] tag. For example, if we wanted to create an AI that does not attack, we can remove the combat CA.

[modify_ai]
    side=2
    action=delete
    path=stage[main_loop].candidate_action[combat]
[/modify_ai]

Changing the Score of a Default AI Candidate Action

The return score is pretty much the only thing that can be changed about a default AI CA (other than the customizing with aspects and goals described at the link above). This can be done using action=change in the [modify_ai] tag. For example, if we want to have an AI that attacks only after the healing, villages and retreat CAs (with default scores 80,000, 60,000 and 40,000) have finished, we can change the combat CA to have a score of 30,000.

[modify_ai]
    side=1
    action=change
    path=stage[main_loop].candidate_action[combat]
    [candidate_action]
        id=combat
        engine=cpp
        name=ai_default_rca::combat_phase
        max_score=30000
        score=30000
    [/candidate_action]
[/modify_ai]

Notes:

  • Note that the change action is really a delete action followed by an add action. Thus, the full content of the [candidate_action] tag needs to be given, not just the parameter that is being changed.
  • Macro versions also exist for these actions.
  • The configurations of the default CAs, including their ids and names, can be found in file data/core/macros/ai_candidate_actions.cfg.

On the Fly Candidate Action Testing

One of the advantages of using Lua for custom AIs is that there is no need for restarting Wesnoth after the code is changed for the changes to take effect. In fact, it is even possible to set things up so that not even a reload is required. You can simply change the code and click on a context menu option (or even just use a keyboard shortcut) to test the new version of the candidate action's evaluation and/or execution function. This section describes how to set up such a testing mechanism using the example of the Guardian Bob AI described above.

The context menu option, along with hotkey 'x', is set up by adding the following code to the WML of whatever scenario you want to test this in.

[set_menu_item]
    id = m01
    description=_"Run Lua test code"
    image=items/ring-red.png~CROP(26,26,20,20)
    [command]
        [lua]
            code=<<wesnoth.dofile "~add-ons/my_addon/lua/test_lua.lua">>
        [/lua]
    [/command]
    [default_hotkey]
        key=x
    [/default_hotkey]
[/set_menu_item]

This should be pretty self-explanatory. The only thing to note is that you need to have a file called 'test_lua.lua' at the path given in the code attribute (or change the path and file name used there, of course). This file could, for example, look like this:

-- Set up CA specific variable
local fn = "~add-ons/my_addon/lua/return_guardian.lua"
local cfg = { id = 'Bob', return_x = 10, return_y = 15 }

-- Clear screen and make sure we're in debug mode
wesnoth.clear_messages()
wesnoth.message('AI testing for side ' .. wesnoth.current.side)
if (not wesnoth.game_config.debug) then
    wesnoth.message("***** This option requires debug mode. Activate by typing ':debug' *****")
    return
end

-- Set up the AI variables needed by the CA
local self = { data = {} }
local ai = wesnoth.debug_ai(wesnoth.current.side).ai

-- Evaluate CA and execute if score > 0
local eval = wesnoth.dofile(fn):evaluation(ai, cfg, self)
wesnoth.message('Eval score:', eval)
if (eval > 0) then
    wesnoth.dofile(fn):execution(ai, cfg, self)
end

The fn and cfg variables define the file containing the CA to be tested, and the configuration table to be passed to the evaluation and execution tables. We assume the same Guardian Bob AI here that is used in the example above.

The next block is simply some "house keeping", cleaning the screen and making sure that we are in debug mode.

We then need to set up the self and ai tables required by the CA functions. self does not need to contain anything other than an empty data table (and even that is not required for our example, we do this here simply to demonstrate how it is done).

By contrast, the ai table needs to be populated with all the Lua AI functions. These are not available by default outside the AI context, but can be accessed through wesnoth.debug_ai(wesnoth.current.side).ai as described here. This requires debug mode to be active, which is why we check for it earlier in the code.

The last few lines are then simply calling the evaluation function and, if it returns a positive score, the execution function, thus simulating the use of the CA by the main_loop stage.

Notes:

  • It is important to realize that we are using wesnoth.dofile() for including files, rather than wesnoth.require(). Using the latter, while recommended for the final version of your code, would require a reload each time a file is changed. This is not needed with wesnoth.dofile(). You can simply change the file and press 'x' again to test the new code.
  • It is, of course, also possible to test other Lua AI (or non-AI) functionality in this way, not just the evaluation and/or execution of CAs. To that end:
    • The Wesnoth Lua Pack debug utilities are an extremely powerful tool for trouble shooting AI code written in Lua; or for figuring out some of the Lua AI functions in the first place. One could, for example, use debug_utils.dbms() to see all the content of the ai table, or check out the structure of the return tables of some of the more complex functions.
  • If you are doing a lot of AI testing, it might be worth to set up a dedicated test scenario with a variety of right-click menu options, for example for CA evaluation only, CA evaluation plus execution, running your own little CA evaluation loop and executing random Lua code. Having several menu options with their own hotkeys in combination with manipulating units on the map in debug mode is a very powerful and convenient method for AI testing.

Legacy Methods for Creating Custom AIs

Several other (older) methods still exist for creating custom AIs. However, there is nothing that can be done with them that cannot also be done with the methods described on this page. While the old methods are still functional at the moment, they might therefore be removed at some point in order to simplify maintenance of the Wesnoth C++ code base. We do not recommend using them for new AI code any more. Ideally, old AI code should be ported to the new methods.

Nevertheless, for the time being the documentation for using the old methods still exists. This includes:


See also: Wesnoth AI

This page was last edited on 15 July 2020, at 19:39.