Formula AI Howto

From The Battle for Wesnoth Wiki
Warning
Formula AI is still functional for the time being, but it is not maintained any more.

That applies to both the code and these wiki pages.

This page is meant as a practical guide for setting up and testing Formula AI functionality in a scenario. It does not give a complete description of the Formula AI language. See FormulaAI and FormulaAI Functions for that. In addition, the first post in this thread links to a file with (not quite complete but useful) information about Formula AI functions and variables.

Make sure you have read about the Wesnoth_AI_Framework as a basic understanding of the purpose of AI engines, stages and candidate actions is assumed here.

Also note that we do not explain all the different ways of setting things up here. Instead, we focus on one specific method that works reasonably well. Using variations thereof will hopefully be straightforward once the principles are understood.

Formula AI Setup

Formula AI does not require an engine to be set up, and (usually) no global variables need to be defined up front either. Thus, only one component needs to be set up, the stage, or stages, controlling Formula AI behavior. Three different stages are available for Formula AI code:

  • Formula AI unit_formulas stage: executes unit-specific Formula AI moves
  • Formula AI side_formulas stage: executes side-wide Formula AI moves
  • RCA AI main_loop stage (cpp): the standard AI stage with its candidate actions

The stages are executed in whichever order they are put into the side tag. That is to say, all the moves of the first stage in the [ai] tag are played out first, then all the moves of the second stage, and so on until the last stage. Then the turn ends.

Setting up the Formula AI unit_formulas Stage

This stage executes formulas that are attached directly to specific units. To set it up, simply put the following code into the AI's [side] tag:

       [ai]
           version=10710
           [stage]
               engine=fai
               name=unit_formulas
           [/stage]
       [/ai]

The first line is required and ensures that we are using the latest version of the AI. 'name' and 'id' keys can also be assigned to the stage, in case we want to remove it later.

Let's now set up a unit that does nothing but move one hex south each turn by adding an [ai] tag to the unit definition:

       [unit]
           x,y=10,4
           id = Rark
           type="Saurian Augur"
           name="Rark"
           [ai]
               formula="move(me.loc, loc(me.loc.x, me.loc.y + 1))"
           [/ai]
       [/unit]

Here, the variable me is set automatically and contains all the unit information, with loc being a table containing its coordinates in x and y sub-fields. See the Testing section below for information on how to query what fields a unit variable such as me contains.

This is all that is needed. The unit defined above will move one hex south each turn until it hits an obstacle. It will do nothing else, as 'move()' results in the movement points being set to zero afterward.

Unit formulas can be attached to any unit on the map. If the order in which the units do their moves matters, a priority can be set:

       [unit]
           ...
           [ai]
               formula="move(me.loc, loc(me.loc.x, me.loc.y + 1))"
               priority=10
           [/ai]
       [/unit]

Units with the highest priority get their moves executed first.

Note: if the above is the only [ai] tag of the side definition, the AI will only do this unit_formulas move and nothing else at all. In order to also have other behavior, other stages, such as those described in the following sections or additional unit_formulas stages, can be added to this [ai] tag (or be placed in their own [ai] tags).

Setting up the Formula AI side_formulas Stage

Side-wide formulas are set up very similarly to the unit-specific formulas. Here is an example of a stage that moves all units with movement points left one hex north:

       [ai]
           version=10710
           [stage]
               engine=fai
               name=side_formulas
               move=" 
                   if( size(units) != 0, 
                       move(units[0].loc, loc(units[0].loc.x, units[0].loc.y - 1)),
                       end
                   ) 

                   where units = filter(my_units, movement_left > 0)
               "
           [/stage]
       [/ai]

The 'version' line is again required and the stage can also be given 'name' and 'id' keys, as before. The main difference here is that we now have one move that applies to all the units of the AI side, resulting in the me variable not being set. Thus, we first need to find all the units of our side (stored in my_units) which have movement left and store them in the units variable (a table). If the length of the table is not zero, the first unit gets moved one hex north. It now has no movement points left (because 'move()' is used, not 'move_partial()') and is therefore not chosen on the next iteration of the side_formulas stage. The stage repeats this action until no unit with movement left is found, in which case the 'if' function returns 'end'.

A few notes:

  • If the code gets too long, it might make more sense to put it into a file and include it with
move={filename.fai}

instead of including it directly as is done above.

  • If this stage is put into the [ai] tags in addition to the unit-specific stage of the previous section, the outcome depends on the order of the stages:
    • If the unit_formulas stage comes first, Rark is moved south, then all other units are moved north.
    • In the opposite order, all units including Rark are first moved north. We then get an error message when the unit_formulas stage tries to move Rark south because he has no movement points left.

Setting up the RCA main_loop Stage and Candidate Actions

The unit_formulas and side_formulas stages are somewhat limited when it comes to complex AI behavior. If a more versatile tool is needed, the candidate actions (CAs) of the RCA AI main_loop stage provide a powerful framework for setting up variable and adaptable AI behavior. Here is how this stage is set up:

       [ai]
           version=10710
           [stage]
               id=main_loop
               name=testing_ai_default::candidate_action_evaluation_loop
               {AI_CA_GOTO}
               {AI_CA_RECRUITMENT}
               {AI_CA_MOVE_LEADER_TO_GOALS}
               {AI_CA_MOVE_LEADER_TO_KEEP}
               {AI_CA_COMBAT}
               {AI_CA_HEALING}
               #{AI_CA_VILLAGES}
               #{AI_CA_RETREAT}
               {AI_CA_MOVE_TO_TARGETS}
               {AI_CA_PASSIVE_LEADER_SHARES_KEEP}

               [candidate_action]
                   engine=fai
                   name=go_south
                   type=movement
                   evaluation="if((me.hitpoints < me.max_hitpoints), 60010, 0)"
                   action="move(me.loc, loc(me.loc.x, me.loc.y + 1))"
               [/candidate_action]
           [/stage]
       [/ai]

Note that we have two of the default CAs commented out. If all of them were used unaltered, we could simply use

    {ai/aliases/stable_singleplayer.cfg}

inside [side] (but outside [ai]) instead. The advantage of listing them specifically is that individual CAs can then be commented out (as is done here) and/or replaced by custom CAs. [Note that it is important not to add a second stage if 'stable_singleplayer.cfg' is included, even if it has the same name and id as the main_loop stage, if your new candidate action is to be evaluated against all the default CAs. See the Micro AI Lurkers test scenario in /data/ai/micro_ais/ for an explanation of how this can be done (yes, that's a Lua AI example, but this particular point applies here as well).]

In this example, the default villages and retreat CAs are replaced by a custom-built CA. This CA uses the fai engine and is given user-defined name go_south. It is of type movement, which means that the evaluation code is called once for every unit of the AI's side that has movement points left, and that this unit is passed to the evaluation and action code in variable me.

In this case, as very simple evaluation returns 60,010 if unit me has less than perfect hitpoints. It returns 0 otherwise. Thus, wounded units that can still move are given an evaluation score below the healing CA (80,000), but above villages (60,000). If this is the highest score of any of the CAs with valid moves left, the action code (note that this is 'action', not 'execution' here) is called, which moves the unit(s) one hex south. (Not a particularly useful CA, but easy to understand for demonstration purposes.)

It should be noted that the commenting out of the villages and retreat CAs is not actually necessary here. If valid go_south moves are found, a score of 60,010 is returned, which is higher than those of villages (60,000) and retreat (40,000). Thus, go_south, if possible, is always executed before these two. As it uses 'move()', not 'move_partial()', which leaves a unit with no movement points, no other movement actions can take place for this unit afterward. [One difference remains however: if the standard CAs are commented out, then no unit will retreat or go to villages. If they are included, only units with non-perfect HP will go south, the rest might still do retreat or go-to-villages moves.]

Other notes:

  • Important: The evaluation functions may not change the game state. Doing that can lead to errors and crashes.
  • More complicated code should, again, be included from a file, for both evaluation and action.
  • The other possible 'type' of candidate action is attack, which is called once for every <me, target> pair, where me is a variable set to a unit on the AI side, and target is a variable set to an enemy unit that me can reach
  • A [filter] tag can be passed to the Formula AI CA that preselects only units passing the filter for evaluation. Thus, the previous candidate action could also be written as:
               [candidate_action]
                   engine=fai
                   name=go_south
                   type=movement
                   [filter]
                       me="filter(input, (input.hitpoints < input.max_hitpoints))"
                   [/filter]
                   evaluation=60010
                   action="move(me.loc, loc(me.loc.x, me.loc.y + 1))"
               [/candidate_action]

For attack type CAs, an equivalent 'target=...' line can be included in the filter.

A Simpler Method for Adding Candidate Actions

The previous method demonstrates the full setup of the RCA AI main_loop stage and candidate actions. If we only want to add stages, or delete individual standard stages, a simpler approach is possible. We can simply add (or delete) these candidate actions using the macros in core/macros/ai.cfg, without the need for defining the stage and standard CAs. For example, the above stage could be added simply as:

       [ai]
           {MODIFY_AI_ADD_CANDIDATE_ACTION {SIDE} main_loop (
               [candidate_action]
                   engine=fai
                   name=go_south
                   type=movement
                   [filter]
                       me="filter(input, (input.hitpoints < input.max_hitpoints))"
                   [/filter]
                   evaluation=60010
                   action="move(me.loc, loc(me.loc.x, me.loc.y + 1))"
               [/candidate_action]
           )}
       [ai]

with no need for a version number or a [stage] tag. Thus, this method is both simpler and does not depend on the number of the most current AI version. See Adding / Deleting Stages and Candidate Actions below for more on this topic and the Formula AI Code Library for examples making use of this method.

Testing Setup

Testing Formula AI code is, in principle, very easy. Simply type 'f' in the Wesnoth window (which accesses the Formula AI command line) and then enter the code to be executed. That's a lot of typing, however. It is easier to put the code into a file (say '~add-ons/FAI_tests/fai_manual.fai') and run it by typing

run_file('~add-ons/FAI_tests/fai_manual.fai')

Still easier is to include that line as a comment in the file itself and copy it to the Formula AI command line. Then we can simply type 'f' and 'cmd-v' (or whatever the paste keyboard shortcut is) to execute our test code. As an example, consider this file:

fai 'fai_manual.fai'
# run_file('~add-ons/FAI_tests/fai_manual.fai') #
# ---- Place test code below ----- #

dir(self)

# ---- Place test code above ----- #
faiend

Here, the first and last line are technically unnecessary, but it is good practice to include them in all Formula AI files as they can be useful for debugging purposes. The second line is the 'run_file' command mentioned above, that can now be easily copy-pasted into the Formula AI command line. Note that comments in Formula AI require the '#' symbol both at the beginning and end of the line.

The body of this example is a very simple, but powerful command: 'dir(self)'. It displays all available Formula AI variables. Its output shows that there is a variable my_units. If we want to see the structure and content of this variable, we replace the body of the file above with

my_units

or type that at the command line, since it is so short. In order to get the first of these unit, use

my_units[0]

and for its location (many Formula AI functions use the unit location)

my_units[0].loc

To select the unit with id "Rark", we type

filter(my_units, id='Rark')[0]

and if we wanted to do this using a function

def get_unit(ai*, unit_id)
    filter(my_units, id = unit_id)[0];

get_unit(self, 'Rark')

In the function call, self passes the set of Formula AI variables to the function, the same set that was queried with 'dir(self)' above. It is put into variable ai in the function, with the '*' denoting ai as an implicit variable (meaning the variable holding the units can be accessed with my_units instead of ai.my_units).

So much for some very simple examples. For full documentation of the Formula AI language see FormulaAI and FormulaAI Functions. We are now ready for testing AI behavior. Let's assume that we have set up a test situation with one of our units (id = "Rark") and an enemy unit (id = "Krar") within reach of Rark. We want to test how to move Rark to the hex south of Krar. The body of '~add-ons/FAI_tests/fai_manual.fai' is now

move(me.loc, loc(target.loc.x, target.loc.y+1))

where me = filter(my_units, id = 'Rark')[0]

where target = filter(enemy_units, id = 'Krar')[0]

The important thing here is that we have now set up a mechanism that creates the me and target variables as they are used in some of the stages described above. We can replace the move used here with whatever AI behavior we want to test. Once we are done with testing, we can simply copy the code above the two 'where' lines into the AI stages or candidate actions.

Note that the Formula AI functions and variables do not need to be initialized. They are available at every turn for each side. Thus, we can simply set up Side 1 with 'controller=human' and start with the testing.

Adding / Deleting Stages and Candidate Actions

Stages and candidate actions can be added and removed at any time in the scenario. A large number of macros are available in core/macros/ai.cfg for this purpose.

Here are two code examples of deleting candidate actions from the default RCA AI stage. The first is done right at the beginning during the side definition:

[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]

The second is done in an event:

[side]
    side=2
    {ai/aliases/stable_singleplayer.cfg}
[/side]
[event]
    name=some_event
    {MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop recruitment}
    {MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop move_leader_to_keep}
[/side]

Examples: Formula AI Code Library

Many examples of Formula AI code can be found found in the Formula AI Code Library page and in this forum thread.

This page was last edited on 24 February 2016, at 20:52.