Formula AI Code Library

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 lists Formula AI code examples that can be used directly in a scenario or as templates for further development. If you have problems with any of them or would like to see additional examples let us know in this forum thread. If you have examples to add, you can also let us know there and we will post them, or feel free to edit these wiki pages yourself.

  • Most of the examples shown on these pages can also be tested in action in campaign "AI Modifications Demos", available on the 1.9/1.10 add-ons server
  • Check out Formula AI Howto for a practical guide for setting up and testing these examples in a scenario.
  • Additional examples can be found in the Wesnoth data directory under ai/formula/. See EditingWesnoth for locating the data directory.

Swamp Lurker Moves

This is the Formula AI adaptation of the WML swamp lurker code from Grnk the Mighty. Swamp lurkers have the 'swamp_submerge' ability, meaning they are invisible in swamps unless an enemy unit is adjacent to them. They are dumb, impulse-drive creatures which move as follows:

  • They can move across most terrain, but only stop on swamp.
  • All lurkers move individually, there is no strategy.
  • If there are enemies within reach (next to swamp terrain), a lurker will attack the unit with the fewest hitpoints.
  • If no unit is in reach, the lurker will move to a random reachable swamp hex.

In Grnk, this behavior is coded in WML (the WML code can be found in '1_unit_macros.cfg' in the Grnk folder or in this forum post). The adaptation to Formula AI follows below. A Lua AI version exists as well, in /data/ai/micro_ais/.

fai 'lurker_moves.fai'
# run_file('~add-ons/AItest/ai/lurker_moves.fai') #

def is_swamp(map, xx, yy)   
    # Tests whether terrain at xx,yy is swamp_water #
    map( 
        filter( map.terrain, (x=xx) and (y=yy)),
        self.id 
    )[0] = 'swamp_water';

def reachable_swamp(unit_loc,map)
    # get all reachable swamp locs for unit at unit_loc #
    # exclude own location #
    filter( 
        unit_moves( unit_loc ), 
        (is_swamp( map, x, y )) and (self != unit_loc)
    );

def random(array)
    # Picks a random elements of 'array' #
    array[(1d size(array) - 1)];

def reachable_enemies_next_to_swamp(unit_loc,attacks,map)
    # Returns all the attacks for enemies that the unit at 'unit_loc' #
    # can reach and that are next to a swamp hex #
    filter( 
        map( attacks.attacks, self),
        (move_from = unit_loc) and (is_swamp( map, attack_from.x, attack_from.y))
    );

def weakest_defender(attacks)
    # Get the enemy with the lowest HP #
    choose( attacks, -unit_at(defender).hitpoints
    );

# Attack if possible, otherwise random move #
if( size(possible_attacks) != 0,
    weakest_defender(possible_attacks),
if( size(swamp_in_reach) != 0,
    move(me.loc,random(swamp_in_reach)),
end)
)

where possible_attacks = reachable_enemies_next_to_swamp( me.loc, my_attacks, map)

where swamp_in_reach = reachable_swamp(me.loc, map)

faiend

This code is meant to be in a file 'lurker_moves.fai. It can be used with the Formula AI unit_moves stage by including

[ai]
    formula={lurker_moves.fai}
[/ai]

in the definition of all swamp lurker units. Or it can be set up as a candidate action that applies to all swamp lurker units of a given side:

[candidate_action]
     engine=fai
     name=lurker_moves
     type=movement
     [filter]
         me="filter(input, (input.type = 'Swamp Lurker'))"
     [/filter]
     evaluation=300000
     action={lurker_moves.fai}
[/candidate_action]

Notes:

  • This code can, of course, also be used with any non-lurker unit as long as it is able to move through swamp.
  • This example currently only works with "pure" swamp, not with quagmire tiles, but the extension to include those should be obvious.

Specifying simple AI combat targets

This Formula AI example is from a multiplayer campaign, The Great Quest, by Coffee.

When the AI has a chance/choice of units to kill, sometimes you might want it to try to kill a certain unit or type of unit in preference of another. The following example makes the AI try to kill a loyal unit in preference of other units where the chance of a kill from one attack is greater than 50% (it then further prioritizes by chance to kill loyal units):

  • Notes: There is a specific exclusion for the AI leader, so it doesn't get goaded into a bad position. There is also a calculation done to attack from the best terrain spot available for each possible attack pair. Calculate_outcome gives a result in the range of 0 to 10000, with >=5000 meaning a 50% chance of success.
[side]
     side={SIDE}
     controller=ai
     #other params here
     [ai]
         #others here such as grouping or aggression
         {MODIFY_AI_ADD_CANDIDATE_ACTION {SIDE} main_loop (
             [candidate_action]
                 id=go_for_loyal_units
                 name=go_for_loyal_units
                 engine=fai
                 type=attack
                 [filter]
                     me="filter(input, 'me', me.leader=0)"
                     target="filter(input, 'target', index_of('loyal',target.traits) != -1)"
                 [/filter]
                 evaluation="if(max_possible_damage(me,target)>=target.hitpoints, if(ca_prob>=5000, {AI_CA_COMBAT_SCORE}+ca_prob,0), 0)
                             where ca_prob=calculate_outcome(me.loc, choose(filter(map(filter(my_moves.moves, src=me.loc), dst), distance_between(self, target.loc) = 1), defense_on(me, self)), target.loc)[1].probability[0]"
                 action="attack(me.loc, choose(filter(map(filter(my_moves.moves, src=me.loc), dst), distance_between(self, target.loc) = 1), defense_on(me, self)), target.loc, -1)"
             [/candidate_action]
         )}
     [/ai]
[/side]

Recruiting certain types of units on particular hexes

This side wide Formula AI example is also taken from The Great Quest by Coffee.

As it currently stands in Wesnoth 1.9, the AI recruits units based on a recruitment_pattern AI parameter. This sometimes can lead to non-optimal scout recruiting, etc. The following code attempts to remedy this by suggesting that certain units be recruited on certain hexes to improve village grabbing, etc. (but could also be used to recruit a wider variety of units within each recruitment_pattern parameter).

This code specifies to recruit an appropriate scout unit (from default faction units) to grab a village 8 tiles away on (7, 24) by flat for side 4. *Note: undead scouts are excluded as ghost only moves 7 hexes and saurians as they move only 6, with quick scout moving 7.

[event]
     name=turn 1
     
     #recruit 8 movement scout unit to get village(7,24) fast
     {MODIFY_AI_ADD_CANDIDATE_ACTION 4 main_loop (
         [candidate_action]
             id=recruit_scout
             name=recruit_scout
             engine=fai
             type=movement
             evaluation="if(turn=1 and unit_at(loc(7,24))=null(), {AI_CA_RECRUITMENT_SCORE}+400, 0)"
             action="recruit(scout_units[(1 d size(scout_units))-1],loc(7,24))
                     where scout_units=map(filter(my_recruits,usage='scout' and undead=0 and id!='Saurian Skirmisher'),id)"
         [/candidate_action]
     )}
[/event]
     
#delete at turn 2, as we should have the wanted scout recruited by then
[event]
     name=turn 2
     
     {MODIFY_AI_TRY_DELETE_CANDIDATE_ACTION 4 main_loop recruit_scout}
[/event]

The evaluation is for turn 1 because this is when we want to recruit this particular unit here. It also checks to see if there is already a unit on this hex as the candidate actions are run more than once. With a score of {AI_CA_RECRUITMENT_SCORE}+400, this formula will be evaluated just before the regular recruitment stage and simply adds to it.

Specifying scouts not attack whilst scouting

In addition to specifying that scouts should be recruited on certain hexes, an aspect can be set so that the AI does not choose to attack whilst it could be scouting instead. An example code for this is:

[side]
  side={SIDE}
  [ai]
     {MODIFY_AI_ADD_ASPECT {SIDE} attacks (
     [facet]
        name=testing_ai_default::aspect_attacks
        id=hold_off_initial_scout_attack
        invalidate_on_gamestate_change=yes
        [filter_own]
           [not]
              type="Cavalryman,Drake Glider,Elvish Scout,Footpad,Ghost,Gryphon Rider,Vampire Bat,Wolf Rider"
           [/not]
        [/filter_own]
     [/facet]
     )}
  [/ai]
[/side]
[event]
  name=turn 3
  {MODIFY_AI_TRY_DELETE_ASPECT {SIDE} attacks hold_off_initial_scout_attack}
[/event]

The above code stops the calculation of combat for the units matching the filter_own Single Unit Filter expression. The turn 3 event deletes this changed behavior (presumably after the scouts have collected a village).

This approach might also be useful for when it is desired that a certain unit or type of unit should not attack. Alternatively, it could be useful to specify that a beserker, say, should not attack unless a formula AI candidate action expression evaluates to > 0 (be careful that you don't rely on the built-in calculation functions though, as they are not executed with this aspect set).

Home guards

A "home guard" is a variant on the "guardian" AI special. With this variant, the unit has an assigned "home" location, and the unit will return there if not involved in combat and if not going to a village, whether for healing or to capture it this turn. (The standard guardian AI will cause the unit to stay where it last attacked.) This differs from the Micro AI (Lua) return guardian in that a home guard will press the attack, possibly getting drawn quite far from "home", rather than returning after each attack. (It can also be lured away by a string of closely-placed villages, but that is something a map builder can control.)

If a home guard is being created at the location it will guard, the following macro can be used (instead of the usual {UNIT} macro).

#define HOME_GUARD_UNIT SIDE TYPE X Y WML
    # Creates a unit of TYPE belonging to SIDE at X,Y, which will
    # return to its spawn location if it has no one to attack (and
    # no need for healing).
    {UNIT {SIDE} {TYPE} {X} {Y} (
        [ai]
            [vars]
                home_loc="loc({X},{Y})"
            [/vars]
        [/ai]
        {WML}
    )}
#enddef

A home guard does not have to be created in the location it will guard though. The following macro can be used after creating a unit to specify a home location independent of where the unit was created (c.f. the core {GUARDIAN} macro).

#define HOME_GUARDIAN X Y
    # Meant to be used as a suffix to a unit-generating macro call.
    # The previously-generated unit will treat (X,Y) as its home.
    [+unit]
        [ai]
            [vars]
                home_loc="loc({X},{Y})"
            [/vars]
        [/ai]
    [/unit]
#enddef

These macros alone are not enough to get home guardian behavior, though; they merely mark the units. In order to get the behavior, a candidate action is required. The following macro defines the candidate action. It can be used much the same as the core "AI_CA_*" macros, either as part of an AI definition or added later, perhaps with {MODIFY_AI_ADD_CANDIDATE_ACTION}.

#define AI_CA_HOME_GUARDIAN
    [candidate_action]
        engine=fai
        name=go_home
        type=movement
        evaluation="if( (null != me.vars.home_loc), {AI_CA_MOVE_TO_TARGETS_SCORE}+10, 0)"
        action="if( (me.loc != me.vars.home_loc), move(me.loc, next_hop(me.loc, me.vars.home_loc)), move(me.loc, me.loc)#do not move this turn#)"
    [/candidate_action]
#enddef

(These macros come from a work-in-progress by JaMiT.)

This page was last edited on 19 April 2023, at 07:56.