FormulaAI/Replacing

From The Battle for Wesnoth Wiki
< FormulaAI
Revision as of 04:08, 12 November 2022 by Celtic Minstrel (talk | contribs) (Throw together a conversion guide)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

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(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), 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]
            evaluate=<<return 10001>>
            execute=<<wesnoth.dofile "ai/lua/guardian.lua">>
            [args]
                guard_radius=3
                guard_x,guard_y=8,5
            [/args]
        [/candidate_action]
    [/ai]
[/unit]

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

local self, cfg, data, filter_own = ...
local F = wesnoth.require "functional"
local attacks = ai.get_attacks()
local me = wesnoth.units.find(filter_own)
local guard_loc = {data.guard_x, data.guard_y}

function is_valid_attack(atk)
    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

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:

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]
                formula=castle
            [/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:

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 u = wesnoth.units.find(select(4, ...))[1]
                if not u then return 0 end
                return ai.check_attack(u,3,12).ok and 10000010 or 0
            >>
            execution=<<
                local u = wesnoth.units.find(select(4, ...))[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 u = wesnoth.units.find(select(4, ...))[1]
                if not u then return 0 end
                return ai.check_attack(u,3,12).ok and 10000009 or 0
            >>
            execution=<<
                local u = wesnoth.units.find(select(4, ...))[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 u = wesnoth.units.find(select(4, ...))[1]
                if not u then return 0 end
                return ai.check_attack(u,3,12).ok and 10000011 or 0
            >>
            execution=<<
                local u = wesnoth.units.find(select(4, ...))[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]
        [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]

Translating Code

to be filled in