FormulaAI/Replacing

From The Battle for Wesnoth Wiki

Note: This page is still a work-in-progess and may contain errors.

Starting in Wesnoth 1.18, the FormulaAI system is officially deprecated and will eventually be removed. This page summarizes the options for replacing old FormulaAI code when porting scenarios.

Broadly speaking, there were three ways to use FormulaAI.

  • Side formulas
  • Unit formulas
  • Candidate actions

Each of these methods now has a direct non-Formula substitute. There are of course many ways in which old FormulaAI code could be ported to the newer Lua AI functions, but this page focuses on the quickest, most direct conversion.

Side Formulas

Side formulas were a special AI stage that would run specific actions, usually prior to the main RCA AI, though it could also substitute entirely.

The closest equivalent outside of FormulaAI is the Lua stage. Other pages on this wiki recommend not using a Lua stage, for good reason, but if you have minimal knowledge of the AI system and just want a scenario to continue functioning as close as possible to the original FormulaAI, then it is simplest option.

Consider the following extremely simple side formula:

[ai]
    [stage]
        engine=fai
        name=side_formulas
        move="{ai/formula/opening.fai}"
    [/stage]
[/ai]

Converting this WML to use the Lua stage is simple:

[ai]
    [stage]
        engine=lua
        code=<<wesnoth.dofile 'ai/lua/opening.lua'>>
    [/stage]
[/ai]

Of course, this is just the easy part. The FormulaAI code also needs to be translated into equivalent Lua code. Fortunately, many of the key functions are almost the same in FormulaAI and Lua – the functions to perform AI actions take the same arguments in both languages, for example.

In this example, the file opening.fai contains the following code. All it does is recruit specific units on the first few turns, as well as spreading the initial recruits out in specific directions.

if(turn = 1, [
	recruit('Skeleton Archer', loc(11,21)),
	recruit('Dark Adept', loc(11,22)),
	recruit('Dark Adept', loc(10,22)),
	recruit('Skeleton Archer', loc(9,22)),
	recruit('Ghost', loc(11,24)),
	move(loc(11,23), loc(14,22)) ],
if(turn = 2, [
	move(loc(11,21),loc(13,17)),
	if(unit_at(loc(11,22)).max_moves = 6,
	move(loc(11,22),loc(13,18)),
	move(loc(11,22),loc(15,19))),
	move(loc(10,22),loc(7,19)),
	move(loc(9,22),loc(4,22)),
	move(loc(11,24),loc(18,24)),
	move(loc(14,22),loc(11,23)),
	recruit('Dark Adept', loc(11,21)),
	recruit('Dark Adept', loc(11,22)) ],
if(turn = 3, [
	move(loc(18,24),loc(20,22)),
	move(loc(15,19),loc(17,17)),
	move(loc(4,22),loc(5,18)),
	recruit('Skeleton Archer', loc(11,21)) ],
if(turn = 4, [
	recruit('Skeleton Archer', loc(11,21)) ],
#else# [
	recruit('Skeleton Archer', loc(11,21)), recruit('Dark Adept', loc(11,22)) ]
))))

It translates to this equally simple Lua code in opening.lua:

if wesnoth.current.turn == 1 then
    ai.recruit('Skeleton Archer', {11,21})
    ai.recruit('Dark Adept', {11,22})
    ai.recruit('Dark Adept', {10,22})
    ai.recruit('Skeleton Archer', {9,22})
    ai.recruit('Ghost', {11,24})
    ai.move({11,23}, {14,22})
elseif wesnoth.current.turn == 2 then
    ai.move({11,21}, {13,17})
    ai.move({11,22}, {13,18})
    ai.move({10,22}, {7,19})
    ai.move({9,22}, {4,22})
    ai.move({11,24}, {18,24})
    ai.move({14,22}, {11,23})
    ai.recruit('Dark Adept', {11,21})
    ai.recruit('Dark Adept', {11,22})
elseif wesnoth.current.turn == 3 then
    ai.move({18,24}, {20,22})
    ai.move({15,19}, {17,17})
    ai.move({4,22}, {5,18})
    ai.recruit('Skeleton Archer', {11,21})
elseif wesnoth.current.turn == 4 then
    ai.recruit('Skeleton Archer', {11,21})
else
    ai.recruit('Skeleton Archer', {11,21})
    ai.recruit('Dark Adept', {11,22})
end

See below for some general tips on converting FormulaAI code to Lua.

Unit Formulas

Although Lua AI does have a direct equivalent of FormulaAI's unit formulas, it's a little more complex than the FormulaAI feature. It's not a special stage that iterates over all the units to run their formulas. Instead, you simply add a candidate action (or a micro AI) directly to the unit.

Using unit formulas required you to add the following code to your side configuration:

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

There is no equivalent requirement when porting this code to the standard Lua AI. However, if you wish to preserve the property that unit formulas always execute before the standard RCA AI, you can add an extra stage for the unit formulas like so:

[ai]
    [stage]
        engine=cpp
        name=ai_default_rca::candidate_action_evaluation_loop
        id=unit_formulas # Can be anything you want, but unit_formulas seems like a good choice.
    [/stage]
[/ai]

That defines an empty stage without any candidate actions, but the unit formulas that you place in specific units will populate it with a candidate action for each unit, as long as you set the stage=unit_formulas key on that candidate action. (Note: This is only supported for candidate actions. Micro AIs placed in a unit's AI configuration will always be placed in the main_loop stage.)

Now, we will consider a few examples uses of unit formulas.

A guardian unit

Consider the following unit definition:

[unit]
    x,y=8,5
    type="Orcish Archer"
    generate_name=yes
    [ai]
        formula="{ai/formula/guardian.fai}"
        [vars]
            guard_radius=3
            guard_loc="loc(8,5)"
        [/vars]
    [/ai]
[/unit]

The guardian.fai file contains the following FormulaAI code:

# If there's someone to attack in range, attack them, otherwise move back to the guarded location #
if(attack, attack, move(me.loc, me.vars.guard_loc))
# We're going to choose the attack that does the most damage #
where attack = choose(
    # Filter all possible AI attacks down to only those that involve this unit and are within my guard radius #
    filter(attacks,
        units = [me.loc] and
        distance_between(me.vars.guard_loc, target) <= me.vars.guard_radius
    ), avg_damage_inflicted
)

There are two approaches to converting this code. The first is a direct conversion, with a custom candidate action. The converted WML would look something like this:

[unit]
    x,y=8,5
    type="Orcish Archer"
    generate_name=yes
    [ai]
        [candidate_action]
            engine=lua
            evaluation=<<return 10001>>
            execution=<<wesnoth.dofile "ai/lua/guardian.lua">>
            [args]
                guard_radius=3
                guard_x,guard_y=8,5
            [/args]
        [/candidate_action]
    [/ai]
[/unit]

The unit formula has been converted to a candidate action. Unit variables are now arguments to the candidate action. Since the inputs must be in WML format, the location had to be split into X and Y variables. This example uses an old-style candidate action because that's slightly easier to set up, but a little more effort could convert it to the recommended style. A score is required for any candidate action; here we choose a fixed score, but it would be better for the evaluation function to check if there's even a move to be made.

And the logic is fairly simple, so it's not that hard to write Lua that does the same thing:

-- This just assigns names to the execution function's inputs.
-- In a new-style candidate action you'd be declaring a method with three parameters,
-- like function my_ca:execute(cfg, data, filter_own)
-- Note the use of a colon instead of a dot, which adds an implicit "self" parameter.
local self, cfg, data, filter_own = ...

-- The "functional" module is useful for WFL-style functional programming
-- It's not required for porting FormulaAI, but it makes the conversion simpler.
local F = wesnoth.require "functional"
local attacks = ai.get_attacks()
-- The unit this CA applies to is the only unit matched by the [filter_own]
local me = wesnoth.units.find(filter_own)[1]
local guard_loc = {data.guard_x, data.guard_y}

-- The original WFL used two higher-order functions, which take a function as input.
-- The functions could be defined inline as is done in WFL, but since Lua is a bit more verbose,
-- it can be easier sometimes to define them ahead of time.
function is_valid_attack(atk)
    -- We want to only consider attacks with the assigned unit (is this right?)
    local src = atk.movements[0].src
    if src.x ~= me.x or src.y ~= me.y then
        return false
    end
    return wesnoth.map.distance_between(guard_loc, attack.target) <= data.guard_radius
end

function avg_damage_inflicted(atk)
    return atk.avg_damage_inflicted
end

-- The actual logic of the action.
-- If there are any attacks to be made, perform the one with the best damage potential.
-- Otherwise, move back to the guarded location.
local attack = F.choose(F.filter(attacks, is_valid_attack), avg_damage_inflicted)
if attack then
    ai.attack(me, attack.target)
else
    ai.move(me, guard_loc)
end

It's a lot longer than the FormulaAI equivalent, but about a third of that is boilerplate that would be needed by any converted unit formula. It also breaks most of the recommendations on how to create custom candidate actions – for example, it's using the older candidate action syntax, and it doesn't even have an evaluation function, just returning a constant value, which means it's likely to be blacklisted. But it does the job.

However, a much simpler solution is to recognize that an appropriate guardian MicroAI already exists with essentially the same behaviour, so you can just use it.

[unit]
    x,y=8,5
    type="Orcish Archer"
    generate_name=yes
    [ai]
        [micro_ai]
            ai_type=stationed_guardian
            station_x,station_y=8,5
            distance=3
        [/micro_ai]
    [/ai]
[/unit]

Approaching a Location

The following simple unit formula instructs a unit to approach the nearest castle, but stop one hex away.

[unit]
    x,y=3,8
    type="Walking Corpse"
    generate_name=yes
    [ai]
        formula="{ai/formula/approach_castle.fai}"
    [/ai]
[/unit]

The FormulaAI code looks like this, expanded out to make the structure clearer:

move(
    me.loc,
    # This looks through all possible locations the unit can move and finds the nearest hex to the target location #
    nearest_loc(
        # Find the nearest castle to the unit #
        nearest_loc(
            me.loc,
            # This line finds all castles on the map #
            map(filter(map.terrain,castle=1), loc)
        ),
        unit_moves(me.loc)
    )
)

A direct conversion might look like this:

[unit]
    x,y=3,8
    type="Walking Corpse"
    generate_name=yes
    [ai]
        [candidate_action]
            evaluate=<<return 10001>>
            execute=<<wesnoth.dofile "ai/lua/approach_castle.lua">>
        [/candidate_action]
    [/ai]
[/unit]

The Lua code looks like this:

local LS = wesnoth.require "location_set"
local filter_own = select(4, ...)
local me = wesnoth.units.get(filter_own)[1]

local castles = wesnoth.map.find{formula = 'castle'}
local nearest_castle = wesnoth.map.nearest_loc(me, castles)
local moves = location_set.of_raw(ai.get_dst_src())

local best_target = wesnoth.map.nearest_loc(nearest_castle, moves[me])

ai.move(me, best_target)

(Note: The function wesnoth.map.nearest_loc does not currently exist, but it could be written using functions that do exist.)

While the above implementation works, it would likely be simpler to recognize that this is exactly what the Goto Micro AI does, so you can just use it:

[unit]
    x,y=3,8
    type="Walking Corpse"
    generate_name=yes
    [ai]
        [micro_ai]
            ai_type=goto
            [filter_location]
                [filter_adjacent]
                    formula=castle
                [/filter_adjacent]
            [/filter_location]
            release_unit_at_goal=yes
        [/micro_ai]
    [/ai]
[/unit]

Patrol Unit

The following unit definition defines a simple patrol route for a unit.

[unit]
    x,y=16,5
    type="Wolf Rider"
    generate_name=yes
    [ai]
        loop_formula="{ai/formula/patrol.fai}"
        [vars]
            guard_radius=3
            waypoints="[ loc(16,5) -> loc(16,15), loc(16,15) -> loc(16,5) ]"
            next_step="loc(16,5)"
        [/vars]
    [/ai]
[/unit]

The FormulaAI code looks like this:

def closest_unit(ai*, me)
	choose(
		enemy_units, 'unit',
		-distance_between(me.loc, unit.loc)
	);

def step_move(me)
	if( unit_at( desired_path[0] ),
		if( desired_path.size > 1,
				move_partial(
					me.loc,
				desired_path[1]
				),
			end
		),
			move_partial(
				me.loc,
			desired_path[0]
		)
	)

where desired_path = shortest_path( me.loc, me.vars.next_step );

def move_ahead(ai*, me)
	if( enemy_units,
		if( distance_between( closest_unit(ai, me).loc, me.loc ) > me.moves-1,
			move_partial(
				me.loc,
				me.vars.next_step
			),

			step_move(me)

		),
		move_partial(
			me.loc,
			me.vars.next_step
		)
);

def patrol_move(ai*, me)
    if( me.vars.next_step = me.loc,
        set_unit_var('next_step',
            me.vars.waypoints[ me.vars.next_step ],
            me.loc
        ),

	move_ahead(ai,me)
    );

if( me.moves = 0,
    end,
    if(attack,
        attack,
        patrol_move(self, me)
    )
)

where attack = if( path_to,
	if(path_to.size <= me.vars.guard_radius,
		attack( me.loc, path_to.last, closest_unit(self, me).loc ),
		0
	),
	0
)

where path_to = if( enemy_units,
	shortest_path( me.loc, closest_unit(self, me).loc ),
	[]
)

As this code is quite a bit more complex than the previous two examples, a direct conversion won't be shown here. After all, there is a Patrol Micro AI that already handles this exact use case, so you can just use it:

[unit]
    x,y=16,5
    type="Wolf Rider"
    generate_name=yes
    [ai]
        [micro_ai]
            ai_type=patrol
            waypoint_x=16,16
            waypoint_y=5,15
            attack_range=3
        [/micro_ai]
    [/ai]
[/unit]

Formula Priorities

One feature of the unit formulas stage was the ability to adjust the priority of actions from one unit to another. Since unit formulas translate to candidate actions, porting this kind of logic is as simple as just setting the candidate action scores.

For example, consider the following code that instructs three units to attack in a specific order:

[unit]
    x,y=3,11
    type="Goblin Spearman"
    generate_name=yes
    [ai]
        formula="attack(me.loc, me.loc, loc(3,12))"
        priority=10
    [/ai]
[/unit]
[unit]
    x,y=3,13
    type="Goblin Spearman"
    generate_name=yes
    [ai]
        priority=9
        formula="attack(me.loc, me.loc, loc(3,12))"
    [/ai]
[/unit]
[unit]
    x,y=2,12
    type="Goblin Spearman"
    generate_name=yes
    [ai]
        priority=11
        formula="attack(me.loc, me.loc, loc(3,12))"
    [/ai]
[/unit]

Using Lua, the code would look something like this:

[unit]
    x,y=3,11
    type="Goblin Spearman"
    generate_name=yes
    [ai]
        [candidate_action]
            engine=lua
            max_score=10000010
            evaluation=<<
                local filter_own = select(4, ...)
                local u = wesnoth.units.find(filter_own)[1]
                if not u then return 0 end
                return ai.check_attack(u,3,12).ok and 10000010 or 0
            >>
            execution=<<
                local filter_own = select(4, ...)
                local u = wesnoth.units.find(filter_own)[1]
                ai.attack(u,3,12)
            >>
        [/candidate_action]
    [/ai]
[/unit]
[unit]
    x,y=3,13
    type="Goblin Spearman"
    generate_name=yes
    [ai]
        [candidate_action]
            engine=lua
            max_score=10000009
            evaluation=<<
                local filter_own = select(4, ...)
                local u = wesnoth.units.find(filter_own)[1]
                if not u then return 0 end
                return ai.check_attack(u,3,12).ok and 10000009 or 0
            >>
            execution=<<
                local filter_own = select(4, ...)
                local u = wesnoth.units.find(filter_own)[1]
                ai.attack(u,3,12)
            >>
        [/candidate_action]
    [/ai]
[/unit]
[unit]
    x,y=2,12
    type="Goblin Spearman"
    generate_name=yes
    [ai]
        [candidate_action]
            engine=lua
            max_score=10000011
            evaluation=<<
                local filter_own = select(4, ...)
                local u = wesnoth.units.find(filter_own)[1]
                if not u then return 0 end
                return ai.check_attack(u,3,12).ok and 10000011 or 0
            >>
            execution=<<
                local filter_own = select(4, ...)
                local u = wesnoth.units.find(filter_own)[1]
                ai.attack(u,3,12)
            >>
        [/candidate_action]
    [/ai]
[/unit]

Candidate Actions

At the WML level, a FormulaAI candidate action and a Lua candidate action look pretty similar. Considering the following example AI with two FormulaAI candidate actions:

[ai]
    [stage]
        id=rca_formulas
        name=ai_default_rca::candidate_action_evaluation_loop
        [candidate_action]
            engine=fai
            name=scouting
            type=movement
            [filter]
                me="filter(input, (input.hitpoints = input.max_hitpoints))"
            [/filter]
            action="{ai/formula/scouting_move.fai}"
            evaluation="{ai/formula/scouting_eval.fai}"
        [/candidate_action]
        [candidate_action]
            engine=fai
            name=level_up_attack
            type=attack
            action="{ai/formula/level_up_attack_move.fai}"
            evaluation="{ai/formula/level_up_attack_eval.fai}"
        [/candidate_action]
    [/stage]
[/ai]

There are already Lua equivalents written for both of these candidate actions, so translating this to Lua is really easy:

[ai]
    [stage]
        id=rca_formulas
        name=ai_default_rca::candidate_action_evaluation_loop
        [candidate_action]
            engine=lua
            name=scouting
            location="ai/lua/ca_simple_scouting.lua"
            max_score = 30000
            [filter_own]
                formula="hitpoints = max_hitpoints"
            [/filter_own]
        [/candidate_action]
        [candidate_action]
            engine=lua
            name=level_up_attack
            location = "ai/lua/ca_level_up_attack.lua"
            max_score = 100000
        [/candidate_action]
    [/stage]
[/ai]

As you can see, the [filter] from the FormulaAI candidate action becomes a [filter_own] on the Lua candidate action. This only works for "me" filters in the FormulaAI, however. An attack formula also has a "target" filter, but Lua candidate actions have no equivalent – you could add the contents of the filter under [args], but the Lua code still needs to be written to read and obey that filter.

Translating Code

to be filled in

This page was last edited on 9 April 2023, at 03:26.