Difference between revisions of "FormulaAI"

From The Battle for Wesnoth Wiki
m (Added a list for keywords)
(move Howto to new "See Also" section)
 
(131 intermediate revisions by 12 users not shown)
Line 1: Line 1:
 +
{| class="wikitable" style="text-align: center;"
 +
|'''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.
 +
|}
 +
 
== Overview ==
 
== Overview ==
  
The Wesnoth Formula AI is an attempt to develop an AI framework for Wesnoth that allows easy and fun development and modification of AIs for Wesnoth.
+
The Wesnoth Formula AI is an attempt to develop an AI framework for Wesnoth that allows easy and fun development and modification of AIs for Wesnoth.  In addition to the technical information on this page, a tutorial-style guide is also available at [[Formula AI Howto]].
  
 
Wesnoth already has support for AIs written in Python, but writing AIs in Python has a couple of problems:
 
Wesnoth already has support for AIs written in Python, but writing AIs in Python has a couple of problems:
Line 8: Line 15:
 
* Python is insecure; a malicious trojan horse Python script masquerading as an AI could do untold damage
 
* Python is insecure; a malicious trojan horse Python script masquerading as an AI could do untold damage
  
The Wesnoth Formula AI aims to create a fairly simple, pure functional language which allows one to implement an AI. It also aims to allow AIs to be tweaked and modified by people with relatively little technical skill; anyone who can use WML should also be able to use the Formula AI to tweak an AI to make the AI in a scenario behave how they want.
+
The Wesnoth Formula AI aims to create a fairly simple, pure functional language which allows one to implement an AI. It also aims to allow AIs to be tweaked and modified by people with relatively little technical programming experience; anyone who can use WML should also be able to use the Formula AI to tweak an AI to make the AI in a scenario behave how they want.
 +
 
 +
== How formulas get called ==
 +
 
 +
=== Formulas Command Line ===
 +
 
 +
To attempt to make it convenient to debug formulas, one can run formulas from within Wesnoth, and see the results. To run a formula, just start game and type 'f'. A command textbox will appear, where you can type a formula, and the results will be printed. For instance, typing
  
The Wesnoth Formula AI is currently in an experimental stage of development. One can play with it and develop a rudimentary AI. Feedback is appreciated.
+
8 + 4
  
To develop an AI using the Formula AI, set ai_algorithm=formula_ai in [side].
+
will result in "12" appearing on the screen. You can now use Wesnoth like a calculator. :-)
  
== Approach ==
+
=== Side-wide formulas ===
 +
The first way to get a formula called is to assign it to a [side] in a WML map. This formula will be called every turn, so it is best used for very simple sides (a couple of special units which should have semi-scripted moves, or a simple handling followed by calls to the standard C++ AI)
  
To use the Formula AI, one should put an [ai] tag inside the [side] tag. Inside this [ai] tag, one should specify the 'move' attribute to be a formula which specifies what movement the AI will make. Each time it's the AI's move, this formula will be run, and the move it results in will be executed. Then the formula will be run again; it'll continue to be run until it stops producing a valid move, at which point the AI will end its turn. Alternatively there is a command that the formula may return which will make it end its turn immediately.
+
To use the Formula AI, one should put an [ai] tag inside the [side] tag. Inside this [ai] tag, one should set up the ''side_formulas'' stage and specify the 'move' attribute to be a formula for the movement the AI will make. Each time it's the AI's move, this formula will be run, and the move it results in will be executed. Then the formula will be run again; it'll continue to be run until it stops producing a valid move, at which point the AI will end its turn. Alternatively there is a command that the formula may return which will make it end its turn immediately.
  
 
A sample AI which does nothing but recruit Wolf Riders is as follows:
 
A sample AI which does nothing but recruit Wolf Riders is as follows:
 +
[side]
 +
    ...
 +
    [ai]
 +
        version=10710
 +
        [stage]
 +
            engine=fai
 +
            name=side_formulas
 +
            move="recruit('Wolf Rider')"
 +
        [/stage]
 +
    [/ai]
 +
[/side]
 +
 +
=== Unit Formulas ===
 +
 +
You can specify a formula for any kind of unit. This is a simple way of doing it:
 +
 +
[unit]
 +
  ...
 +
  [ai]
 +
    formula="move(me.loc, loc(me.loc.x, me.loc.y - 1))"
 +
  [/ai]
 +
[/unit]
 +
 +
Above formula will simply move unit one hex to the north every turn. Note how "me" keyword allows access to unit itself.  In addition, the ''unit_formulas'' stage also needs to be set up in the side definition:
  
 
  [side]
 
  [side]
  ...
+
    ...
  ai_algorithm=formula_ai
+
    [ai]
 +
        version=10710
 +
        [stage]
 +
            engine=fai
 +
            name=unit_formulas
 +
        [/stage]
 +
    [/ai]
 +
  [/side]
 +
 
 +
If you need to execute unit formulas in a specific order, you can set a priority:
 +
 
 +
[unit]
 +
  ...
 +
  [ai]
 +
    formula="move(me.loc, loc(me.loc.x, me.loc.y - 1))"
 +
    priority=10
 +
  [/ai]
 +
[/unit]
 +
 
 +
Units with highest priority get their formula executed first.
 +
 
 +
You can also define AI unit-specific variables and use them in you formulas:
 +
 
 +
  [unit]
 +
  ...
 
   [ai]
 
   [ai]
  move="recruit('Wolf Rider')"
+
    formula="if(attack, attack, move(me.loc, me.vars.guard_loc) )
 +
        where attack = choose(filter(attacks, units = [me.loc] and distance_between(me.vars.guard_loc, target) <= me.vars.guard_radius and unit_at(target).side=me.vars.hostile_side-1 ), avg_damage_inflicted)"
 +
 +
    [vars]
 +
      guard_radius=3
 +
      guard_loc="loc(8,5)"
 +
      hostile_side=1
 +
    [/vars]
 
   [/ai]
 
   [/ai]
  [/side]
+
  [/unit]
 +
 
 +
This formula will get location position from variable guard_loc and make sure that unit attacks only opponent from side 1 (value specified by hostile_side variable) which is 3 hexes (guard_radius) or less from guard_loc.
 +
 
 +
Types of variables that are supported:
 +
*number:
 +
variable=3
 +
*text (important: note the ' ' within " "):
 +
name="'I am variable'"
 +
*list:
 +
number_list=[ 1, 2, 3]
 +
* map:
 +
map=[ 'Elvish Archer' -> 70, 'Elvish Shaman' -> 60 ]
 +
*location:
 +
place="loc(X,Y)"
 +
 
 +
=== Candidate move evaluation ===
 +
==== Overview ====
 +
Units in wesnoth have all sort of special abilities, special powers and special usages. Thus, it was needed to have an easy way to describe special behaviour for special units. Adding an formula to a unit only partially solves the problem. It allows easily to have special units (like ennemy heroes) have special behaviour but not normal units with special powers to to behave smartly.
 +
 
 +
Candidate moves are a way to have formula that will
 +
 
 +
* evaluate a given situation
 +
* see if they are able to provide "special moves" for the case they know how to handle
 +
* give a move to do if the situation match their condition
  
== Formula Access ==
+
==== Evaluation Algorithm ====
  
To attempt to make it convenient to debug formulas, one can run formulas from within Wesnoth, and see the results. To run a formula, just type 'f'. A command textbox will appear, where you can type a formula, and the results will be printed. For instance, try pressing 'f' and then type "8 + 4" and the result, "12" will appear. You can now use Wesnoth like a calculator. :-)
+
Each side will have some candidate move blocks made of
 +
* a name for the move
 +
* a type (see below for description of the different types and how they are called)
 +
* a formula returning a score for the move
 +
* a formula executing the move to do
  
== Formula Basics ==
+
The engine will run the following pseudo-code
  
The Formula language supports basic arithmetic operations, such as +, -, *, /, etc. It also supports equality, = and !=, and comparison operators, <, >, <=, and >=. 'false' values are 0 (integer) and null. Other values are true. It supports integers but does NOT support decimal or floating point numbers.
+
while (a formula returned a score greatern than 0) {
 +
      foreach (registered candidate move) {
 +
          foreach (set of parameters for that candidate move's type) {
 +
              call the evaluation formula, note the score returned
 +
          }
 +
      }
 +
      if (the highest score returned was greater than 0) {
 +
          call the candidate move that returned the highest score with the corresponding set of parameters
 +
      }
 +
}
  
It also supports common operators such as and, or, and not. These are examples of valid formulas
+
in other word, we evaluate all candidate moves, then run the candidate move with the highest score, then re-evaluate everything, until no candidate move wants to play anymore
  
4 + 8*7 (evaluates to 60)
+
==== Syntax ====
(4 + 8)*7 (evaluates to 84)
+
To add a new candidate move to the RCA AI ''main_loop'' stage, use the following syntax:
5 and 0 (evaluates to 0)
 
  
== AI Formula Language ==
+
      [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]
  
Of course, the formula language must be able to access information about the scenario being played to make intelligent decisions. Thus there are various 'inputs' that one may access. A simple example of an input is the turn number one is on, given by the input, 'turn'. Try bringing up the formula textbox using 'f' and then type in 'turn'. The AI will print out the current turn number the game is on.
+
This is an example of a candidate action that gets added to all the standard RCA AI candidate actions and moves each unit with movement points left one hex south. It is also possible to use
 +
      {ai/aliases/stable_singleplayer.cfg}
 +
inside the [side] tag to set up the standard RCA AI candidate actions, and then add the customized candidate actions later using [modify_ai] or one of the available macros.
  
The 'turn' input is a simple integer. However, some inputs are complex types which contain other inputs, or which may be lists of inputs. For instance, the input 'my_units' contains a list of all the AI's units.
+
Each [candidate_action] block describes a candidate move and must have the following fields
  
A complex input such as a unit will contain a variety of inputs inside it. If one has a unit input, called 'u' for instance, one can access the 'x' co-ordinate of that unit by using u.loc.x -- u.loc accesses the 'location' object inside the 'unit' object, and the 'location' object contains 'x' and 'y' inputs inside it, which are the x and y co-ordinate of the unit.
+
* ''name'' : the name of this candidate block
 +
* ''type'' : see the paragraph about types below, this describe the implicite variables that the formulas will be called with
 +
* ''evaluation'' : a formula returning an integer. This number reflects how good the move is compared to other moves. Values of zero will mean the move is not a good idea and should be skipped
 +
* ''action'' : A formula that will be called if that candidate move is the best one
  
== Functions ==
+
==== Candidate move types ====
  
The formula language contains a large number of built-in functions which allow you to carry out all kinds of complex tasks. A simple example of a function is the 'if' function. The 'if' function takes three parameters; if the first parameter is true, the function will evaluate to being equal to its second input, otherwise it will evaluate to being equal to its third input.
+
there are two types of candidate moves
  
For instance, an AI that recruits Wolf Riders on the first turn, and Orcish Grunts thereafter might look like this:
+
* '''attack''' : these are called once for every pair <me,target> where me is a variable set to a unit on the AI side, and target is a variable set to an ennemy unit that "me" can reach
 +
* '''movement''' : these are called once for every unit on the AI side, the "me" variable is set to the corresponding unit
  
move="if(turn = 1, recruit('Wolf Rider'), recruit('Orcish Grunt'))"
+
=== Summary : a typical AI turn ===
  
There are a wide variety of functions which can be used to accomplish many different tasks. You can also define your own functions (see below).
+
An AI turn will run the following formulas. Any formula returning an "end turn" move will (of course) finish the turn. But assuming this doesn't happen :
 +
* All unit formulas are called once
 +
* All candidate moves are evaluated and played
 +
* All ''move='' formulas are called until they don't return valid moves
 +
* If no ''move='' formula is provided, we drop automatically to C++
 +
* Turn is ended.
  
== Keywords ==
+
Note that this means that you have to explicitely call "fallback" or set up the RCA AI ''main_loop'' stage specifically, if you want the C++ AI to finish your moves after a side-wide formula.  Also note that the stages are executed in the order in which they are put into the [ai] tag, not in the order of the above list.
  
The formula language has some reserved keywords to provide primitive functionality.  Currently the following keywords are defined:
+
== AI Formula Language ==
  
* functions: Returns a list of all built-in and custom functions available to the AI
+
For a detailed coverage of the language itself, see [[Wesnoth Formula Language]]. This section covers some things specific to the use of the language in FormulaAI.
  
* def: This keyword creates functions using the syntax def function_name(arg1, arg2, ....) function_body.  For example, 
+
=== Overview ===
    def sum(x,y) x + y
 
creates a function sum taking two arguments and returns their sum.
 
  
== Lists ==
+
The formula language must be able to access information about the scenario being played to make intelligent decisions. Thus there are various 'inputs' that one may access. A simple example of an input is the turn number one is on, given by the input, 'turn'. Try bringing up the formula command line using 'f' and then type in
  
The formula language has support for lists. A list is a sequence of values. For instance, 'my_units' is a list of unit objects. A list is represented as square brackets, [], surrounding a comma-seperated list. For instance, [4, 8, 7] is a list of three numbers.
+
turn
  
There are various functions which operate on lists:
+
The AI will print out the current turn number the game is on.
  
size(list): number of items in the list EXAMPLE: size(my_units) -- number of units the AI controls
+
The 'turn' input is a simple integer. However, some inputs are complex types which contain other inputs, or which may be lists of inputs. For instance, the input 'my_units' contains a list of all the AI's units.
  
head(list): the first item in the list EXAMPLE: head(my_units) -- the first unit in the list of units.
+
A complex input such as a unit will contain a variety of inputs inside it. If one has a unit input, called 'u' for instance, one can access the 'x' co-ordinate of that unit by using u.loc.x -- u.loc accesses the 'location' object inside the 'unit' object, and the 'location' object contains 'x' and 'y' inputs inside it, which are the x and y co-ordinate of the unit.
 +
=== Available variables ===
 +
these are variables that you can call in an AI formula or from the command line.
  
map(list, formula): will run 'formula' on each item in the list, and evaluate to a new list which contains the same number of items as in 'list', with the formulas run on each item. EXAMPLE: map(my_units, unit.hitpoints) -- will give a list back with the number of hitpoints each unit has. This is more useful in conjunction with other functions.
+
Some of these variables are complicated data structues, calling their name from the formula command line will allow you to easily see their content
  
filter(list, formula): will run 'formula' on each item in the list. Will evaluate to a list which only contains items the formula was true for. EXAMPLE: filter(my_units, hitpoints < max_hitpoints) -- will return all of your units which have less than maximum hitpoints. For instance this could be used if looking for candidates for healing.
+
use the '''dir''' and '''debug_print''' to inspect the variables
  
sum(list): evaluates to the sum of the items in the list (which all must be numbers) EXAMPLE: sum(map(my_units, max_hitpoints - hitpoints)) -- finds the total damage your units have taken.
+
to get a coplete list, use the  
 +
dir(self)
 +
command
  
max(list) and min(list): evaluates to the maximum or minimum item in the list (which all must be numbers) EXAMPLE: max(my_units, level) -- finds the maximum level of the units you have.
 
  
choose(list, formula): evaluates 'formula' for each item in the list. Will evaluate to the item which 'formula' gave the highest value. EXAMPLE: to give back the unit with the highest level, choose(my_units, level).
+
* ''attacks'' a list of all possible attacks
 +
NOTE: this variable returns a list of all possible attacks (including attack combinations - attacking a single enemy unit with several units) , which is immediately treated as list of attack orders, which is evaluated (so, evaluation of this variable results in attacks being made). If you want to get the list without such evaluation, wrap it inside the array, "[attacks]". If you want to get the first attack, wrap it inside the array twice, "[[attacks[0]]]"
  
sort(list, formula): evaluates to a list sorted according to the comparison 'formula' for each item 'a' and its successor 'b'. For instance, sorting units according to hitpoints would be done by sort(my_units, a.hitpoints > b.hitpoints).
+
* ''my_attacks'' a list of all possible attacks. It returns slightly different list then ''attacks''. I.e. when only two units (attacker and dedenter) are on the board ''attacks'' return one attack, from the position with the highest defence and ''my_attacks'' returns all 6 attacks from all adjacent tiles.
 +
* ''my_side'' global variables describing your own side
 +
* ''teams'' all the data about all the teams
 +
* ''turn'' the current turn number
 +
* ''time_of_day'' the current time of day
 +
* ''keeps'' all keep locations
 +
* ''vars'' all localy set varables using the set_var function
 +
* ''allies'' a list of all sides that are friendly
 +
* ''ennemies'' a list of all sides that are enemy
 +
* ''my_moves'' a list of all my moves
 +
* ''enemy_moves'' a list of all enemy moves
 +
* ''my_leader'' our leader unit
 +
* ''my_recruits'' the list of units that can be recruited by us
 +
* ''recruits_of_side'' foreach side the list of units that can be recruited
 +
* ''units'' the complete list of all units
 +
* ''units_of_side'' foreach side, a list of all owned units
 +
* ''my_units'' the complete list of all units owned by the current player
 +
* ''enemy_units'' all units that are enemy to the current player
 +
* ''villages'' all the villages on the map
 +
* ''my_villages'' all the villages owned by the current player
 +
* ''villages_of_side'' for each side, the list of villages owned
 +
* ''enemy_and_unowned_villages'' all villages that you don't own
 +
* ''map'' all the data about the map
  
 +
=== Functions ===
  
== Custom functions ==
+
* The formula language contains a large number of built-in functions which allow you to carry out all kinds of complex tasks. List of these functions, usage and simple examples can be found [[FormulaAI_Functions|here]].
  
You can define your own functions. A function is a formula which takes some inputs as parameters. Suppose we wanted to define a function that puts some value on a unit, we might add the following to the [ai] tag:
+
* You can define your own functions in WML. A function is a formula which takes some inputs as parameters. Suppose we wanted to define a function that puts some value on a unit, we might add the following to the [ai] tag:
  
 
  [function]
 
  [function]
 
  name=value_unit
 
  name=value_unit
  inputs=unit
+
  inputs="unit"
 
  formula="unit.hitpoints + unit.level*4"
 
  formula="unit.hitpoints + unit.level*4"
 
  [/function]
 
  [/function]
  
This has defined a new function which takes a unit as an input, and runs the given calculation over it.
+
This has defined a new function which takes a 'unit' as an input, and runs the given calculation over it.
 +
 
 +
* We can have multiple inputs in our functions, to define them, just create comma-separated inputs list:
 +
 
 +
inputs="attacker,defender"
 +
 
 +
This has defined a new function which takes both 'attacker' and 'defender' as an inputs.
  
== Unit Formulas ==
+
* Sometimes, we use one of our inputs really often in our function - to make our life easier we can make its members (inputs) directly accessible from within the formula. This is improved version of function from above:
  
You san specify a formula for any kind of unit. This is a simple way of doing it:
+
[function]
 +
name=value_unit
 +
inputs="unit*"
 +
formula="hitpoints + level*4"
 +
[/function]
  
[unit]
+
As you can see, if we define input with a * char at the end, we make it a 'default input' for a formula. Note, that you can define only one default input per function.
  ...
+
 
  formula="move(me.loc, loc(me.loc.x, me.loc.y - 1))"
+
* It is important to know difference between formulas defined in custom functions, and formula defined by a 'move=' in a [ai] tag. For example, if we want to get info about leader, we write in formula 'my_leader' - which acces member of the AI. To be able to use 'my_leader' in custom functions we have to add 'ai' as an input for our function:
[/unit]
+
 
 +
inputs="ai"
 +
 
 +
allows us to access leader info by writing 'ai.my_leader', or:
 +
 
 +
inputs="ai*"
 +
 
 +
allows us to access leader info by simply writing 'my_leader'
  
Custom unit formulas are executed first at the begining of side's turn. Above formula will simply move unit one hex to the north every turn. Note how "me" keyword allows access to unit itself.
+
=== Files and formulas ===
  
You can also define AI unit-specific variables and use them in you formulas:
+
You can easily split your formulas between different files and include them when necessary. For example:
  
 
  [unit]
 
  [unit]
 
   ...
 
   ...
   formula="if(attack, attack, move(me.loc, choose(unit_moves(me.loc), -distance_between(self, me.vars.guard_loc))))
+
   formula={my_unit_formula.fai}
  where attack = choose(filter(attacks, units = [me.loc] and distance_between(me.vars.guard_loc, target) <= me.vars.guard_radius), avg_damage_inflicted)"
 
  [ai_vars]
 
    guard_radius=3
 
    guard_loc="loc(8,5)"
 
  [/ai_vars]
 
 
  [/unit]
 
  [/unit]
  
This formula will get location position from variable guard_loc and make sure that unit attacks opponent which is 3 hexes (value specified by guard_radius variable) or less from guard_loc.
+
Will look for unit formula in the my_unit_formula.fai file.
 +
 
 +
Although it is not mandatory, we advocate to use following syntax in your fai files:
 +
 
 +
faifile '<filename>'
 +
...
 +
faiend
 +
 
 +
This makes formula system know which file it is working with now, and gives you improved error reporting, which is often really helpful. Valid syntax for my_unit_formula.fai would be:
 +
 
 +
faifile 'my_unit_formula.fai'
 +
...
 +
faiend
 +
 
 +
== Tool Support ==
 +
 
 +
=== ctags ===
 +
 
 +
For some rudimentary support for exuberant ctags, add the following to .ctags (or create the file if it doesn't exist):
 +
 
 +
--langdef=formulaai
 +
--langmap=formulaai:.fai
 +
--regex-formulaai=/^def[ \t]*([a-zA-Z0-9_]+)/\1/d,definition/
 +
 
 +
This is especially nice when used with an editor or plugin with ctags support, such as Taglist for Vim.
 +
 
 +
=== Vim ===
  
Types of variables that are supported:
+
====Syntax Highlighting====
*number:
+
 
variable=3
+
Follow these steps to enjoy vim syntax highlighting support for Formula AI.
*text (important: note the ' ' within " "):
+
 
  name="'I am variable'"
+
# Grab the Formula AI vim syntax file, [http://svn.gna.org/viewcvs/*checkout*/wesnoth/trunk/data/tools/vim/formulaai.vim formulaai.vim].
*list:
+
# Copy formulaai.vim to .vim/syntax
number_list=[ 1, 2, 3]
+
# Add the following to .vimrc :
*location:
+
  autocmd! BufRead,BufNewFile *.fai setfiletype formulaai
  place="loc(X,Y)"
+
 
 +
====Taglist Support====
 +
 
 +
First you will need the very nice [http://www.vim.org/scripts/script.php?script_id=273 taglist plugin]. Follow the link for downloads and install directions if you don't already have it installed.
  
 +
Next, you'll need Formula AI support for exuberant ctags, follow the instructions in the [[#ctags|ctags]] section. 
  
 +
Once you have all that, simply add the following line to your .vimrc:
 +
 +
let tlist_formulaai_settings = 'formulaai;d:definition'
  
 +
To test it all out, open a Formula AI script file and enter the command
 +
:Tlist
  
 +
You should now have some nice highlighting and be able to easily navigate through formula, enjoy!
 +
 
  
 +
==See Also==
 +
* [[Wesnoth_AI]]
 +
* [[FormulaAI/Replacing]]
 +
* [[Formula_AI_Howto]]
  
[[Category:Development]]
+
[[Category:AI]]

Latest revision as of 15:21, 7 April 2023

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.

Overview

The Wesnoth Formula AI is an attempt to develop an AI framework for Wesnoth that allows easy and fun development and modification of AIs for Wesnoth. In addition to the technical information on this page, a tutorial-style guide is also available at Formula AI Howto.

Wesnoth already has support for AIs written in Python, but writing AIs in Python has a couple of problems:

  • it's still rather difficult, especially for a non-programmer, to develop an AI, even in Python
  • Python is insecure; a malicious trojan horse Python script masquerading as an AI could do untold damage

The Wesnoth Formula AI aims to create a fairly simple, pure functional language which allows one to implement an AI. It also aims to allow AIs to be tweaked and modified by people with relatively little technical programming experience; anyone who can use WML should also be able to use the Formula AI to tweak an AI to make the AI in a scenario behave how they want.

How formulas get called

Formulas Command Line

To attempt to make it convenient to debug formulas, one can run formulas from within Wesnoth, and see the results. To run a formula, just start game and type 'f'. A command textbox will appear, where you can type a formula, and the results will be printed. For instance, typing

8 + 4 

will result in "12" appearing on the screen. You can now use Wesnoth like a calculator. :-)

Side-wide formulas

The first way to get a formula called is to assign it to a [side] in a WML map. This formula will be called every turn, so it is best used for very simple sides (a couple of special units which should have semi-scripted moves, or a simple handling followed by calls to the standard C++ AI)

To use the Formula AI, one should put an [ai] tag inside the [side] tag. Inside this [ai] tag, one should set up the side_formulas stage and specify the 'move' attribute to be a formula for the movement the AI will make. Each time it's the AI's move, this formula will be run, and the move it results in will be executed. Then the formula will be run again; it'll continue to be run until it stops producing a valid move, at which point the AI will end its turn. Alternatively there is a command that the formula may return which will make it end its turn immediately.

A sample AI which does nothing but recruit Wolf Riders is as follows:

[side]
    ...
    [ai]
        version=10710
        [stage]
            engine=fai
            name=side_formulas
            move="recruit('Wolf Rider')"
        [/stage]
    [/ai]
[/side]

Unit Formulas

You can specify a formula for any kind of unit. This is a simple way of doing it:

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

Above formula will simply move unit one hex to the north every turn. Note how "me" keyword allows access to unit itself. In addition, the unit_formulas stage also needs to be set up in the side definition:

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

If you need to execute unit formulas in a specific order, you can set a priority:

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

Units with highest priority get their formula executed first.

You can also define AI unit-specific variables and use them in you formulas:

[unit]
 ...
  [ai]
    formula="if(attack, attack, move(me.loc, me.vars.guard_loc) )
       where attack = choose(filter(attacks, units = [me.loc] and distance_between(me.vars.guard_loc, target) <= me.vars.guard_radius and unit_at(target).side=me.vars.hostile_side-1 ), avg_damage_inflicted)"

    [vars]
      guard_radius=3
      guard_loc="loc(8,5)"
      hostile_side=1
    [/vars]
  [/ai]
[/unit]

This formula will get location position from variable guard_loc and make sure that unit attacks only opponent from side 1 (value specified by hostile_side variable) which is 3 hexes (guard_radius) or less from guard_loc.

Types of variables that are supported:

  • number:
variable=3
  • text (important: note the ' ' within " "):
name="'I am variable'"
  • list:
number_list=[ 1, 2, 3]
  • map:
map=[ 'Elvish Archer' -> 70, 'Elvish Shaman' -> 60 ]
  • location:
place="loc(X,Y)"

Candidate move evaluation

Overview

Units in wesnoth have all sort of special abilities, special powers and special usages. Thus, it was needed to have an easy way to describe special behaviour for special units. Adding an formula to a unit only partially solves the problem. It allows easily to have special units (like ennemy heroes) have special behaviour but not normal units with special powers to to behave smartly.

Candidate moves are a way to have formula that will

  • evaluate a given situation
  • see if they are able to provide "special moves" for the case they know how to handle
  • give a move to do if the situation match their condition

Evaluation Algorithm

Each side will have some candidate move blocks made of

  • a name for the move
  • a type (see below for description of the different types and how they are called)
  • a formula returning a score for the move
  • a formula executing the move to do

The engine will run the following pseudo-code

while (a formula returned a score greatern than 0) {
     foreach (registered candidate move) {
          foreach (set of parameters for that candidate move's type) {
              call the evaluation formula, note the score returned
          }
     }
     if (the highest score returned was greater than 0) {
         call the candidate move that returned the highest score with the corresponding set of parameters
     }
}

in other word, we evaluate all candidate moves, then run the candidate move with the highest score, then re-evaluate everything, until no candidate move wants to play anymore

Syntax

To add a new candidate move to the RCA AI main_loop stage, use the following syntax:

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

This is an example of a candidate action that gets added to all the standard RCA AI candidate actions and moves each unit with movement points left one hex south. It is also possible to use

      {ai/aliases/stable_singleplayer.cfg}

inside the [side] tag to set up the standard RCA AI candidate actions, and then add the customized candidate actions later using [modify_ai] or one of the available macros.

Each [candidate_action] block describes a candidate move and must have the following fields

  • name : the name of this candidate block
  • type : see the paragraph about types below, this describe the implicite variables that the formulas will be called with
  • evaluation : a formula returning an integer. This number reflects how good the move is compared to other moves. Values of zero will mean the move is not a good idea and should be skipped
  • action : A formula that will be called if that candidate move is the best one

Candidate move types

there are two types of candidate moves

  • attack : these are called once for every pair <me,target> where me is a variable set to a unit on the AI side, and target is a variable set to an ennemy unit that "me" can reach
  • movement : these are called once for every unit on the AI side, the "me" variable is set to the corresponding unit

Summary : a typical AI turn

An AI turn will run the following formulas. Any formula returning an "end turn" move will (of course) finish the turn. But assuming this doesn't happen :

  • All unit formulas are called once
  • All candidate moves are evaluated and played
  • All move= formulas are called until they don't return valid moves
  • If no move= formula is provided, we drop automatically to C++
  • Turn is ended.

Note that this means that you have to explicitely call "fallback" or set up the RCA AI main_loop stage specifically, if you want the C++ AI to finish your moves after a side-wide formula. Also note that the stages are executed in the order in which they are put into the [ai] tag, not in the order of the above list.

AI Formula Language

For a detailed coverage of the language itself, see Wesnoth Formula Language. This section covers some things specific to the use of the language in FormulaAI.

Overview

The formula language must be able to access information about the scenario being played to make intelligent decisions. Thus there are various 'inputs' that one may access. A simple example of an input is the turn number one is on, given by the input, 'turn'. Try bringing up the formula command line using 'f' and then type in

turn

The AI will print out the current turn number the game is on.

The 'turn' input is a simple integer. However, some inputs are complex types which contain other inputs, or which may be lists of inputs. For instance, the input 'my_units' contains a list of all the AI's units.

A complex input such as a unit will contain a variety of inputs inside it. If one has a unit input, called 'u' for instance, one can access the 'x' co-ordinate of that unit by using u.loc.x -- u.loc accesses the 'location' object inside the 'unit' object, and the 'location' object contains 'x' and 'y' inputs inside it, which are the x and y co-ordinate of the unit.

Available variables

these are variables that you can call in an AI formula or from the command line.

Some of these variables are complicated data structues, calling their name from the formula command line will allow you to easily see their content

use the dir and debug_print to inspect the variables

to get a coplete list, use the

dir(self)

command


  • attacks a list of all possible attacks

NOTE: this variable returns a list of all possible attacks (including attack combinations - attacking a single enemy unit with several units) , which is immediately treated as list of attack orders, which is evaluated (so, evaluation of this variable results in attacks being made). If you want to get the list without such evaluation, wrap it inside the array, "[attacks]". If you want to get the first attack, wrap it inside the array twice, "[[attacks[0]]]"

  • my_attacks a list of all possible attacks. It returns slightly different list then attacks. I.e. when only two units (attacker and dedenter) are on the board attacks return one attack, from the position with the highest defence and my_attacks returns all 6 attacks from all adjacent tiles.
  • my_side global variables describing your own side
  • teams all the data about all the teams
  • turn the current turn number
  • time_of_day the current time of day
  • keeps all keep locations
  • vars all localy set varables using the set_var function
  • allies a list of all sides that are friendly
  • ennemies a list of all sides that are enemy
  • my_moves a list of all my moves
  • enemy_moves a list of all enemy moves
  • my_leader our leader unit
  • my_recruits the list of units that can be recruited by us
  • recruits_of_side foreach side the list of units that can be recruited
  • units the complete list of all units
  • units_of_side foreach side, a list of all owned units
  • my_units the complete list of all units owned by the current player
  • enemy_units all units that are enemy to the current player
  • villages all the villages on the map
  • my_villages all the villages owned by the current player
  • villages_of_side for each side, the list of villages owned
  • enemy_and_unowned_villages all villages that you don't own
  • map all the data about the map

Functions

  • The formula language contains a large number of built-in functions which allow you to carry out all kinds of complex tasks. List of these functions, usage and simple examples can be found here.
  • You can define your own functions in WML. A function is a formula which takes some inputs as parameters. Suppose we wanted to define a function that puts some value on a unit, we might add the following to the [ai] tag:
[function]
name=value_unit
inputs="unit"
formula="unit.hitpoints + unit.level*4"
[/function]

This has defined a new function which takes a 'unit' as an input, and runs the given calculation over it.

  • We can have multiple inputs in our functions, to define them, just create comma-separated inputs list:
inputs="attacker,defender"

This has defined a new function which takes both 'attacker' and 'defender' as an inputs.

  • Sometimes, we use one of our inputs really often in our function - to make our life easier we can make its members (inputs) directly accessible from within the formula. This is improved version of function from above:
[function]
name=value_unit
inputs="unit*"
formula="hitpoints + level*4"
[/function]

As you can see, if we define input with a * char at the end, we make it a 'default input' for a formula. Note, that you can define only one default input per function.

  • It is important to know difference between formulas defined in custom functions, and formula defined by a 'move=' in a [ai] tag. For example, if we want to get info about leader, we write in formula 'my_leader' - which acces member of the AI. To be able to use 'my_leader' in custom functions we have to add 'ai' as an input for our function:
inputs="ai"

allows us to access leader info by writing 'ai.my_leader', or:

inputs="ai*"

allows us to access leader info by simply writing 'my_leader'

Files and formulas

You can easily split your formulas between different files and include them when necessary. For example:

[unit]
 ...
 formula={my_unit_formula.fai}
[/unit]

Will look for unit formula in the my_unit_formula.fai file.

Although it is not mandatory, we advocate to use following syntax in your fai files:

faifile '<filename>'
...
faiend

This makes formula system know which file it is working with now, and gives you improved error reporting, which is often really helpful. Valid syntax for my_unit_formula.fai would be:

faifile 'my_unit_formula.fai'
...
faiend

Tool Support

ctags

For some rudimentary support for exuberant ctags, add the following to .ctags (or create the file if it doesn't exist):

--langdef=formulaai
--langmap=formulaai:.fai
--regex-formulaai=/^def[ \t]*([a-zA-Z0-9_]+)/\1/d,definition/

This is especially nice when used with an editor or plugin with ctags support, such as Taglist for Vim.

Vim

Syntax Highlighting

Follow these steps to enjoy vim syntax highlighting support for Formula AI.

  1. Grab the Formula AI vim syntax file, formulaai.vim.
  2. Copy formulaai.vim to .vim/syntax
  3. Add the following to .vimrc :
autocmd! BufRead,BufNewFile *.fai setfiletype formulaai

Taglist Support

First you will need the very nice taglist plugin. Follow the link for downloads and install directions if you don't already have it installed.

Next, you'll need Formula AI support for exuberant ctags, follow the instructions in the ctags section.

Once you have all that, simply add the following line to your .vimrc:

let tlist_formulaai_settings = 'formulaai;d:definition'

To test it all out, open a Formula AI script file and enter the command

:Tlist

You should now have some nice highlighting and be able to easily navigate through formula, enjoy!


See Also

This page was last edited on 7 April 2023, at 15:21.