Exercises in Formula and Lua AI and AI-demos add-on feedback

Discussion of all aspects of the game engine, including development of new and existing features.

Moderator: Forum Moderators

Post Reply
Anonymissimus
Inactive Developer
Posts: 2461
Joined: August 15th, 2008, 8:46 pm
Location: Germany

Re: Exercises in Formula and Lua AI

Post by Anonymissimus »

(I didn't read the other post, just answering your question.)
mattsc wrote: Is this a scenraio that's on the add-ons server?
Yes. The moveto goal is in 15_Save_the_heir.cfg in "The Earth's Gut". The ai healing its units code is independent from that and is in The_Earths_Gut/utils/ai_controller.cfg and it's used in the above scenario and in 06_The_great_gates.cfg.
projects (BfW 1.12):
A Simple Campaign: campaign draft for wml startersPlan Your Advancements: mp mod
The Earth's Gut: sp campaignSettlers of Wesnoth: mp scenarioWesnoth Lua Pack: lua tags and utils
updated to 1.8 and handed over: A Gryphon's Tale: sp campaign
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Anonymissimus: I'll have a look at that. Give me a little time, this is a busy week coming up. I'll get back to you.

SimonS: Thanks for the suggestions. Sounds like things I should be able to implement, and they might even be useful (unlike the other things I have done so far which are really just academic exercises).

Just as an aside, the last scenario of "Grnk the Mighty" (Wizards) has guardians that return to their posts (although I think they follow enemy units for as long as any are in reach and only return to their post afterward, so it's not quite the same as what you suggest). In that scenario I did that using WML. Using an AI modification would probably be "cleaner" (and a nice little exercise for me), so I'll definitely look into that.
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Simons: I looked into the guardian AI you mentioned and programming that is not a problem at all, but I do have a couple conceptual questions.
Simons Mith wrote:Behaviour would be, if anything moves close enough to guarding_x,guarding_y, attack it. If there's nothing close enough to attack, go back to stationed_at_x,stationed_at_y. If there is something, but attacking it would mean you were more than one turn's movement from stationed_at_x,stationed_at_y, then return to stationed_at_x,stationed_at_y instead of attacking. (This is an expansion on the basic idea where stationed_at_ and guarding_ are the same location.)
I don't quite understand how the guarding and stationed positions are supposed to work together. You say "if anything moves close enough to guarding_x,guarding_y, attack it." What does "close enough" mean in this context? The guarding unit's max_moves? If so, you could get into the situation where the approaching unit is on the other side of the guarding position (GP) as seen from the station position (SP), and exactly one unit's turn from GP. That makes it farther than max_moves from SP, in contradiction to the second part of your description. If you attack only units within max_moves of SP, then I don't understand the significance of the GP.

The other question (let's assume GP=SP for now): Say a unit moves in, so that our guard can just reach it. The guard attacks and is no exactly max_moves from from GP. On the next turn, the unit is still there (or another unit moved in to the same distance from GP). If we stay where we are and attack, everything's good. However, if left to the standard AI, the guard could be moved to the opposite side of the enemy, if the terrain is better over there, and could end up max_moves+2 from the guarding position.

A similar problem happens if there is now a second unit within reach of the guard, but outside the "defense distance". The standard attack might then choose that second unit. As far as I know, you cannot eliminate targets (or positions around targets) for select units only when using the default AI attack. That only works for all attacks of a side.

How do you want to deal with that? One possibility would be to have the guard simply attack whatever enemy unit is closest to GP, from whatever hex has the highest defense rating, as long as that hex is within one turn's reach of GP. That could be programmed easily without having to use the default attack action.
Simons Mith wrote:I'm proposing something that could be called with {GUARDIAN stationed_at_x, stationed_at_y, guarding_x, guarding_y}
That macro name is already taken for the standard guardian. Do you have a different suggestion?
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

As a side comment/question, there are several pathfinder functions that I would like to have for Lua AI programming that, I think, don't exist. Some of them are variations of existing functions, others would provide some functionality that isn't there. Or at least I have not found it yet.

next_hop(unit, x, y): Same as the respective Formula AI function. Provides the next position 'unit' can get to on its path to (x,y) with its current MP. It's a sub-functionality of what WLP's [find_path] does. This one I've written (stolen):

Code: Select all

    function next_hop(unit, x, y)
    -- Finds the next "hop" of 'unit' on its way to (x,y)
    -- Returns coordinates of the endpoint of the hop, and movement cost to get there
        local path, cost = wesnoth.find_path(unit, x, y)

        -- If unit cannot get there:
        if cost >= 42424242 then return nil, cost end

        -- If unit can get there in one move:
        if cost <= unit.moves then return {x, y}, cost end

        -- If it takes more than one move:
        local next_hop, nh_cost = {x,y}, 0
        for index, path_loc in ipairs(path) do
            local sub_path, sub_cost = wesnoth.find_path( unit, path_loc[1], path_loc[2])

            if sub_cost <= unit.moves 
                then next_hop, nh_cost = path_loc, sub_cost
                else return next_hop, nh_cost
            end
        end
    end
movement_cost_between(unit, x1, x2, y1, y2): same as distance_between(), but not returning the distance between the two positions, but the movement cost it would take a unit to get from (x1,y1) to (x2,y2). I would like this to work even if that unit is not at the first position (and even if there is a different unit at that position), so find_path() will not work for this.

find_theoretical_reach(unit, x1, x2): find all the hexes a unit could reach if it were at (x1,y1) and if no other units were around. Again, I want this to work even if there is another unit at (x1,y1), which means that the existing functions cannot be used (without taken that unit off the map and putting a temporary unit in that position - I would like to avoid that as it would mean "blinking" of the unit on the map and probably unnecessary overhead).

As far as I know the latter two do not exist and are not super-trivial modification of existing functions. I don't think writing them is all that hard and I can give it a shot, but if somebody has done so already or if I am missing something, I'd be happy not to reinvent the wheel.
Anonymissimus
Inactive Developer
Posts: 2461
Joined: August 15th, 2008, 8:46 pm
Location: Germany

Re: Exercises in Formula and Lua AI

Post by Anonymissimus »

mattsc wrote:find_theoretical_reach(unit, x1, x2): find all the hexes a unit could reach if it were at (x1,y1) and if no other units were around. Again, I want this to work even if there is another unit at (x1,y1), which means that the existing functions cannot be used (without taken that unit off the map and putting a temporary unit in that position - I would like to avoid that as it would mean "blinking" of the unit on the map and probably unnecessary overhead).
I think wesnoth.find_reach with ignore_units=true *and* a *private lua proxy unit* should do the trick.

Generally, if you think that one of the engine functions are useful to expose in the lua interface or could be much more useful with modifications, then that can be done rather easily probably. If only lua is needed it could be added to helper. Not currently though since there's a feature freeze.
Other than that it could be added to the WLP to escape the freeze.
projects (BfW 1.12):
A Simple Campaign: campaign draft for wml startersPlan Your Advancements: mp mod
The Earth's Gut: sp campaignSettlers of Wesnoth: mp scenarioWesnoth Lua Pack: lua tags and utils
updated to 1.8 and handed over: A Gryphon's Tale: sp campaign
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Anonymissimus wrote:I think wesnoth.find_reach with ignore_units=true *and* a *private lua proxy unit* should do the trick.
I tried that, it doesn't work. wesnoth.find_reach when called with a unit as argument seems to only use that to get the coordinates. It then finds the reach for whatever unit is on the map at those coordinates, not the private proxy unit. (Confused the hell out of me at the beginning!)
Anonymissimus wrote:Generally, if you think that one of the engine functions are useful to expose in the lua interface or could be much more useful with modifications, then that can be done rather easily probably.
Cool! The main problem with that is that I am not (yet) all that familiar with the engine functions. Do you know of anything that could be used here?
Anonymissimus wrote:If only lua is needed it could be added to helper. Not currently though since there's a feature freeze.
Other than that it could be added to the WLP to escape the freeze.
Personally, I think these functions would be useful to have available (other than in my private library). I think next_hop() is ready to be added to WLP or helper (after the freeze), but one of you more experienced guys should have a look at it (also to see if it is worth it).
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Simon: I slapped together a slightly simpler version of your suggested guardian AI. The whole attack thing as you suggested it is a bit more complicated in the details (not undoable by any means, just more work than I have time for right now). Also, for now guard position (GP) and stationed position (SP) are the same, waiting for further information from you as requested above. The current version does the following:

- If at GP with no enemy in reach, do nothing
- If at GP with enemy in reach, leave attack to default AI (note that this could include not attacking if the enemy is deemed too strong)
- If not at GP, return there, no matter whether an enemy is in reach or not.
- If enemies are blocking your way back, do your best to get there anyway
- If you end up next to an enemy on the way back, attack afterward

The code is attached below. Unfortunately, it consists of 3 separate macros (2 of them could be combined if the guardian units were always on the map at the scenario start, but if you want flexibility to add guardians later, you need these three). To implement this, put:
{RETURN_GUARDIAN_PRELOAD} inside the [scenario] tag
{RETURN_GUARDIAN_ENGINE} inside the side definition
and as many of these as you have guardian units:
{RETURN_GUARDIAN_UNIT SIDE ID GUARD_X GUARD_Y}
where the 4 arguments need to be replaced by the respective values. This needs to be placed inside action WML (an event or similar) after the unit has been put on the map. Note that the unit does not need to be at the guard position when it is first put on the map. It will make its way there on its own.

I know that this is not quite what you asked for, but why don't you give it a try first just to make sure that it works at all. Then we can talk about modifications. I wrote this using 1.9.10. I don't know off the top of my head which version is required. Hope it is useful!

Ok, I really need to do something else for a while now. :)

EDIT: This is the original version of the code. A newer version might be available here.

Code: Select all

#define RETURN_GUARDIAN_PRELOAD
    [event]
        name=preload
        first_time_only=no
        [lua]
            code = <<
                H = wesnoth.require "lua/helper.lua"
                W = H.set_wml_action_metatable {}
                _ = wesnoth.textdomain "my-campaign"

                -- Define your global constants here.
                ai = {}
                ca_counter = 0
                H.set_wml_var_metatable(_G)
            >>
        [/lua]
    [/event]
#enddef

#define RETURN_GUARDIAN_ENGINE
    {ai/aliases/stable_singleplayer.cfg}
    [ai]
        [engine]
            name="lua"
            code= <<
--! ==============================================================
ai = ...

local guardian_ai = {}

function guardian_ai:next_hop(unit, x, y)
-- Finds the next "hop" of 'unit' on its way to (x,y)
-- Returns coordinates of the endpoint of the hop, and movement cost to get there
    local path, cost = wesnoth.find_path(unit, x, y)

    -- If unit cannot get there:
    if cost >= 42424242 then return nil, cost end

    -- If unit can get there in one move:
    if cost <= unit.moves then return {x, y}, cost end

    -- If it takes more than one move:
    local next_hop, nh_cost = {x,y}, 0
    for index, path_loc in ipairs(path) do
        local sub_path, sub_cost = wesnoth.find_path( unit, path_loc[1], path_loc[2])

        if sub_cost <= unit.moves 
            then next_hop, nh_cost = path_loc, sub_cost
            else return next_hop, nh_cost
        end
    end
end

function guardian_ai:guardian_evaluation(id, to_x, to_y)

    local unit = wesnoth.get_units { id=id }[1]

    if (unit.x~=to_x or unit.y~=to_y) then 
        value = 100010
    else
        value = 99990
    end

    --print("Eval:", value)
    return value
end

function guardian_ai:guardian_execution(id, to_x, to_y)

    local unit = wesnoth.get_units { id=id }[1]
    --print("Exec guardian move",unit.id)

    local nh = self:next_hop(unit, to_x, to_y)
    if unit.moves~=0 then 
        ai.move_full(unit, nh[1], nh[2])
    end
end

return guardian_ai
--! ==============================================================
>>
        [/engine]
    [/ai]
#enddef

#define RETURN_GUARDIAN_UNIT SIDE ID GUARD_X GUARD_Y
    [add_ai_behavior]
        side={SIDE}
        [filter]
             id="{ID}"
        [/filter]
	    sticky=yes
	    loop_id=main_loop
        evaluation="return (...):guardian_evaluation('{ID}', {GUARD_X}, {GUARD_Y})"
        execution="(...):guardian_execution('{ID}', {GUARD_X}, {GUARD_Y})"
    [/add_ai_behavior]
#enddef
Last edited by mattsc on November 27th, 2011, 11:31 pm, edited 1 time in total.
User avatar
Simons Mith
Posts: 821
Joined: January 27th, 2005, 10:46 pm
Location: Twickenham
Contact:

Re: Exercises in Formula and Lua AI

Post by Simons Mith »

mattsc wrote:Simons: I looked into the guardian AI you mentioned and programming that is not a problem at all, but I do have a couple conceptual questions.
Simons Mith wrote:Behaviour would be, if anything moves close enough to guarding_x,guarding_y, attack it. If there's nothing close enough to attack, go back to stationed_at_x,stationed_at_y. If there is something, but attacking it would mean you were more than one turn's movement from stationed_at_x,stationed_at_y, then return to stationed_at_x,stationed_at_y instead of attacking. (This is an expansion on the basic idea where stationed_at_ and guarding_ are the same location.)
I don't quite understand how the guarding and stationed positions are supposed to work together. You say "if anything moves close enough to guarding_x,guarding_y, attack it." What does "close enough" mean in this context? The guarding unit's max_moves? If so, you could get into the situation where the approaching unit is on the other side of the guarding position (GP) as seen from the station position (SP), and exactly one unit's turn from GP. That makes it farther than max_moves from SP, in contradiction to the second part of your description. If you attack only units within max_moves of SP, then I don't understand the significance of the GP.
<snip>

While implementing this, I think two things to bear in mind are 1) assume shroud and 2) assume a unit without any hostiles within current visual range does nothing, even if it's not at its stationed_at location.

The reason I say that is as follows:
Consider a unit placed with movement of 6 placed at 5,5 using a [unit] tag, guarding 10,5 and with stationed_at set to 15,5.

Such a unit isn't at its stationed_at location, but nevertheless it just sits there like an ordinary guard because there's nothing within range to wake it up. Assuming shroud is helpful here, because it demonstrates that the unit should act oblivious to anything outside its movement radius.

Sooner or later, a hostile unit will enter its detection radius, and it will wake up. Two possibilities, one much more likely than the other. The most likely possibility is that it can't engage the hostile, so it has to move towards its stationed_at location instead. If the hostile appeared at, say 11,5, then it could do both, by moving towards 15,5, and winding up next to the target, in which case it could attack. Then in future rounds it could keep on attacking, because now it's close enough to its stationed_at location that it's legally allowed to carry on dishing out punishment. But if it then saw a new hostile at 8,5, the presense of that unit in its sensor radius would cause it to move towards 15,5, rather than towards the 8,5 unit.

Rereading what I initially wrote, the wording wasn't ideal. Instead of 'go back to stationed_at_x,stationed_at_y' I should have said move towards stationed_at_x,stationed_at_y - because the unit isn't necessarily ever going to reach its stationed_at location, unless there are lots of hostiles constantly darting about just out of reach.
Simons Mith wrote:I'm proposing something that could be called with {GUARDIAN stationed_at_x, stationed_at_y, guarding_x, guarding_y}
That macro name is already taken for the standard guardian. Do you have a different suggestion?
(Shrug) STATIONED_GUARDIAN?
 
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Oh, I see. I figured a unit would always move back to its station if nothing else is going on. I'll have to think about what this means and how to do it.
Anonymissimus
Inactive Developer
Posts: 2461
Joined: August 15th, 2008, 8:46 pm
Location: Germany

Re: Exercises in Formula and Lua AI

Post by Anonymissimus »

mattsc wrote:
Anonymissimus wrote:I think wesnoth.find_reach with ignore_units=true *and* a *private lua proxy unit* should do the trick.
I tried that, it doesn't work. wesnoth.find_reach when called with a unit as argument seems to only use that to get the coordinates. It then finds the reach for whatever unit is on the map at those coordinates, not the private proxy unit. (Confused the hell out of me at the beginning!)
Right. It actually just gets the coordinates of the unit, passes them to the pathfinder but then gets again the unit on them.

That's unfortunate, it doesn't seem the pathfinder requires the unit to be in the unit_map, and it appears to affect all of the exposed pathfinder functions. So I can try to remove the limitation, it's probably a good idea to require the unit to have valid on-board coordinates though.
projects (BfW 1.12):
A Simple Campaign: campaign draft for wml startersPlan Your Advancements: mp mod
The Earth's Gut: sp campaignSettlers of Wesnoth: mp scenarioWesnoth Lua Pack: lua tags and utils
updated to 1.8 and handed over: A Gryphon's Tale: sp campaign
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Anonymissimus wrote:That's unfortunate, it doesn't seem the pathfinder requires the unit to be in the unit_map, and it appears to affect all of the exposed pathfinder functions. So I can try to remove the limitation
Yes, please!! From my point of view that would be really, really useful.
Anonymissimus wrote:it's probably a good idea to require the unit to have valid on-board coordinates though.
Agreed.
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Anonymissimus wrote:I have one scenario in my campaign where the AI is supposed to perform a "moveto-goal". Basically what the player does with Konrad in HttT scenario 1.
I'm currently doing it with complicated wml.
-If you're free to move, move towards the goal hex.
-If you're wounded, heal until full health in one of some defined villages, then move again.
-If you're trapped, don't do anything.
That's about the minimum requirements.
If possible, things like
-Don't move too quickly away from friendly units into the direction of many enemies.
-If possible and this doesn't slow down moving towards the target hex, attack with no retaliation.
Anonymissimus: I have coded your 3 minimum requirements, or at least my interpretation thereof. I implemented this using the map 15_Save_the_heir.map from 'The Earth's Gut', with the same way points and village locations as you use in that scenario (hardcoded, for now, but it should be obvious how to generalize that). Here's what the AI does (it works for as many units simultaneously as you want):

Move toward goals: This is left to the standard goto candidate action, as you have it in your scenario. All you need to do is set goto_x and goto_y to the first way point when you create the unit(s). Leaving the moves to the goto CA is different in two ways from moving the unit directly, both of which can be considered desired or annoying, depending on your preferences:
  • The unit will take a more or less straight route toward the goal, which might not be the quickest route
  • The unit will end its turn when it reaches a way point, even if it has MP left
A clear advantage (in terms of ease of coding) of using the goto action is that we don't need to remember which waypoint a unit was heading toward if it interrupts its movement for healing etc. It will simply resume its course afterward.

Retreat to villages for healing: Any unit with hitpoints < max_hitpoints will find the closed unoccupied village from the pre-defined list, retreat there, and stay there until fully healed. If no unoccupied village is found, it will continue on its route toward the next way point.

Do nothing when trapped: This happens automatically because no other actions are included in the AI. Well, if ZoC'ed, the unit will still do the one move it might have toward healing or the goals, which I think is useful. You'd still want the unit to move toward the next village, however slowly.

So that's the current status. Let me know if this works for you or if you want me to change anything. As for the other 2 actions:

Don't move too quickly away from friendly units into the direction of many enemies: How do we quantify this? An option would be to calculate for each hex the distance from all units on the map, counting friendly units as positive, enemy units as negative. If the move decreases that number by more than a certain threshold, stay in place. Did you have something else/better in mind?

If possible and this doesn't slow down moving towards the target hex, attack with no retaliation: So I guess this means only attack units that cannot fight back for the chosen attack? And only if you don't move in range of other enemies on their next turn? Is that what you had in mind? That doesn't sound very likely to happen all that often. In addition, we'd have to quantify "if it does not slow down moving toward the goals". Attack only if the enemy is in direction of the goal? Seems even more unlikely in combination with the other 2 conditions. I probably misunderstood what you mean.


Finally, here's the code, one preload event:
EDIT: This is the original version of the code. A newer version might be available here.

Code: Select all

    [event]
        name=preload
        first_time_only=no
        [lua]
            code = <<
                H = wesnoth.require "lua/helper.lua"
                W = H.set_wml_action_metatable {}
                _ = wesnoth.textdomain "my-campaign"

                -- Define your global constants here.
                ai = {}
                --konrad_ai = {}
                ca_counter = 0
                H.set_wml_var_metatable(_G)
            >>
        [/lua]
    [/event]
and the engine to include in the side definition (I think I've taken all the hard-coded and testing stuff out of it, but let me know if I forgot something)

Code: Select all

    [ai]
        version=10710
        [engine]
            name="lua"
            code= <<
--! ==============================================================
ai = ...

local konrad_ai = {}

-- Finds the next "hop" of 'unit' on its way to (x,y)
-- Returns coordinates of the endpoint of the hop, and movement cost to get there
function konrad_ai:next_hop(unit, x, y)

    local path, cost = wesnoth.find_path(unit, x, y)

    -- If unit cannot get there:
    if cost >= 42424242 then return nil, cost end

    -- If unit can get there in one move:
    if cost <= unit.moves then return {x, y}, cost end

    -- If it takes more than one move:
    local next_hop, nh_cost = {x,y}, 0
    for index, path_loc in ipairs(path) do
        local sub_path, sub_cost = wesnoth.find_path( unit, path_loc[1], path_loc[2])

        if sub_cost <= unit.moves 
            then next_hop, nh_cost = path_loc, sub_cost
            else return next_hop, nh_cost
        end
    end
end

function konrad_ai:village_retreat_evaluation()

    -- Find all wounded units with MP left
    self.data.units_to_heal = wesnoth.get_units 
        { side = wesnoth.current.side, 
            formula = "(hitpoints < max_hitpoints) and ($this_unit.moves > 0)" 
        }

    -- Store them in the AI's data structure and
    -- return score just higher than goto CA
    local score = 0
    if self.data.units_to_heal[1] then
        score = 201000
    end

    --print("Village retreat eval:", score)
    return score
end

function konrad_ai:village_retreat_execution()

    local villages = { {13,7}, {25,12}, {28,16}, {30,21} }

    -- For each wounded unit
    for i,u in ipairs(self.data.units_to_heal) do
        --print("Heal unit:",u.id)

        -- Find the closest unoccupied village (if not occupied by unit itself)
        local d_min, j_min = 9999, -1
        for j,v in ipairs(villages) do
            local uov = wesnoth.get_units { x=v[1], y=v[2], { "not", { id = u.id } } }[1]
            if not uov then
                d = H.distance_between(u.x,u.y,v[1],v[2])
                --print("  distance to villages #",j,d)
                if d < d_min then d_min = d; j_min = j end
            end
        end
        --print("  -> min distance:",j_min,d_min)

        -- If an available village was found, move toward it, otherwise just sit there
        if j_min ~= -1 then
            -- Find "next hop" on route to village
            local nh = self:next_hop(u, villages[j_min][1], villages[j_min][2])
            --print("  -> move to:", nh[1], nh[2])

            -- Move unit to village
            ai.move_full(u, nh[1], nh[2])
        end
    end

    self.data.units_to_heal = nil
end

function konrad_ai:change_goto_evaluation()

    -- Is there a unit at one of the waypoints?
    local unit_wp1 = wesnoth.get_units { side = wesnoth.current.side, x = 13, y = 9 }[1]
    local unit_wp2 = wesnoth.get_units { side = wesnoth.current.side, x = 16, y = 8 }[1]

    -- If so, return score just lower than goto CA
    local score = 0
    if unit_wp1 or unit_wp2 then
        score = 199990
    end

    --print("Change goto eval:", score)
    return score
end

function konrad_ai:change_goto_execution()

    -- We can simply modify the units at the waypoints
    -- If there isn't one, nothing will happen
    H.modify_unit( { side = wesnoth.current.side, x = 13, y = 9 }, { goto_x = 16, goto_y = 8 } )
    H.modify_unit( { side = wesnoth.current.side, x = 16, y = 8 }, { goto_x = 35, goto_y = 23 } )

end

return konrad_ai
--! ==============================================================
>>
        [/engine]
            [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=lua
                    name=change_goto
                    evaluation="return (...):village_retreat_evaluation()"
                    execution="(...):village_retreat_execution()"
                [/candidate_action]
                [candidate_action]
                    engine=lua
                    name=change_goto
                    evaluation="return (...):change_goto_evaluation()"
                    execution="(...):change_goto_execution()"
                [/candidate_action]
            [/stage]
    [/ai]
Last edited by mattsc on November 27th, 2011, 11:32 pm, edited 1 time in total.
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Simons Mith: I think I have written the entire coward AI behavior as you suggested. I will post the code later, I need to clean it up a bit first. For now, here's how I have implemented the decision making (which direction to choose) for your or anybody's comments.

First, if no enemy is within 'radius', the unit will do nothing, just sit there.

If one or more enemies are within 'radius', we first calculate a "distance rating" for all hexes our unit can reach. I chose 1/d^2 for this (d = distance from enemy unit), summed over all enemy units within 'radius'. You can think of this as the "gravitational pull" the unit would feel from the enemy, if all enemies had equal masses. The unit will try to go to the hex that has the lowest "pull" from the enemies. For me, that makes perfect sense as a criterion. :geek: Look at the first attached image below. There's a coward wolf being approached by 3 enemies. The numbers below each hex give the value of the criterion I just mentioned (*1000). If no 'seek' or 'avoid' location is given, the wolf will simply go to the hex with the lowest value, indicated by the gold ring.

Now, if you want to use 'seek' and 'avoid' as a "tie breaker", it's not that easy because there is often a single hex with the lowest value. Thus, we choose all hexes that have no more than twice the minimum score. These are indicated by the rings (of any color). For those hexes, we now calculate another score. It gets a positive contribution from 'seek' and a negative contribution from 'avoid'. In practice, I use this:

Code: Select all

1 / (d_seek + 1)  -   1  / (d_avoid + 1)^2 * 0.75
I use the square for d_avoid, but not d_seek so that seek has a "larger range" than avoid. Otherwise, if avoid is in between the unit and seek, it will overpower the signal from seek and the unit will never get there. The '+1' is simply so that I don't get divides by zero. And the '*0.75' gives preference to seek if seek and avoid are the same position and the unit is next to it. All of this is somewhat arbitrary, of course, but it seems to work pretty well. Let's look at some more examples:

In the second image, I have added a seek target at the bottom right (indicated by the archery target). The rings (of any color) again show the pre-selected hexes. The 3 white and gold rings are those that have the best (and same) criterion for seek. To pick the final hex to move to, we now choose that one (from the 3 finalists) that has the lowest original distance-from-enemy score, meaning the gold ring with a score of 53. Thus, since going to the left and right is almost the same in this case, the seek target on the right will make the wolf flee to the right.

Now, third image, add an avoid location, indicated by the orcish banner. Seek will still pull the unit to the right, but it will try to avoid the banner location, thus veering to the top - closer to one of the enemies, but the best compromise when taking all three criteria into account.

As a final example, you requested the option of only having to provide either x or y, but not both, for seek and avoid. If you don't give x, then the distance to seek or avoid is simply the difference between the unit and target's y values; and vice versa. An example is given in the last image, where I have used no avoid location, and seek has only a y value equal to the bottom of the screen. Thus, the unit will now try to make it to the bottom of the map, while avoiding enemies as well as possible.

So there it is. As I said, the method chosen is somewhat arbitrary, but it seems to give the desired result. When you get around to it, why don't you play through a number of different setups and let me know if you want me to tweak something.
Attachments
seek and avoid are not set
seek and avoid are not set
seek set to bottom right corner
seek set to bottom right corner
seek set to bottom right corner, avoid set to orcish banner location
seek set to bottom right corner, avoid set to orcish banner location
avoid not set, seek_x not set, seek_y set to bottom of map
avoid not set, seek_x not set, seek_y set to bottom of map
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

And here comes the code. This requires BfW 1.9.10 as is uses [enemy_of] in a SSF.

Same as for the guardian, there are 3 macros:
{COWARD_PRELOAD}: place inside the [scenario] tag
{COWARD_ENGINE}: place inside side definition
{COWARD_UNIT SIDE ID RADIUS SEEK_X SEEK_Y AVOID_X AVOID_Y}: to be included in action WML after the unit definition. If you don't want to set certain coordinates, just use any non-numerical string incl. the empty string.

This can be done for as many units on a side as desired. Other units on the same side will follow the normal AI behavior.

A couple things I did not mention before:
- The coward unit only takes enemies within RADIUS into account. That means that it might run straight into the arms of another enemy that is just outside that range. I think that's ok and consistent with what a panicked animal might do.
- The unit will only run from units on non-allied sides.

Enjoy.

EDIT: Code updated to take care of the problem mentioned in the next post
EDIT 2: This is the original version of the code. A newer version might be available here.

Code: Select all

#define COWARD_PRELOAD
    [event]
        name=preload
        first_time_only=no
        [lua]
            code = <<
                H = wesnoth.require "lua/helper.lua"
                W = H.set_wml_action_metatable {}
                _ = wesnoth.textdomain "my-campaign"

                -- Define your global constants here.
                --ai = {}
                ca_counter = 0
                H.set_wml_var_metatable(_G)
            >>
        [/lua]
    [/event]
#enddef

#define COWARD_ENGINE
    {ai/aliases/stable_singleplayer.cfg}
    [ai]
        [engine]
            name="lua"
            code= <<
--! ==============================================================
local ai = ...

local coward_ai = {}

function coward_ai:filter(input, condition)
    -- equivalent of filter() function in Formula AI

    local filtered_table = {}

    for i,v in ipairs(input) do
        if condition(v) then
            --print(i, "true")
            table.insert(filtered_table, v)
        end
    end

    return filtered_table
end

function coward_ai:generalized_distance(x1, y1, x2, y2)
    -- determines "distance of (x1,y1) from (x2,y2) even if 
    -- x2 and y2 are not necessarily both given (or not numbers)

    -- Return 0 if neither is given
    if (not x2) and (not y2) then return 0 end

    -- If only one of the parameters is set
    if (not x2) then return math.abs(y1 - y2) end 
    if (not y2) then return math.abs(x1 - x2) end 

    -- Otherwise, return standard distance
    return H.distance_between(x1, y1, x2, y2)
end

function coward_ai:coward_evaluation(id)

    local unit = wesnoth.get_units{ id = id }[1]

    if unit.moves > 0 then
        return 300000
    else
        return 0
    end
end

function coward_ai:coward_execution(id, radius, seek_x, seek_y, avoid_x, avoid_y)

    local unit = wesnoth.get_units{ id = id }[1]
    local reach = wesnoth.find_reach(unit)
    -- enemy units within reach
    local enemies = wesnoth.get_units { 
        { "filter_side", {{"enemy_of", {side = wesnoth.current.side} }} }, 
        { "filter_location", {x = unit.x, y = unit.y, radius = radius} }
    }

    -- if no enemies are within reach: keep unit from doing anything and exit
    if not enemies[1] then 
        ai.stopunit_all(unit)  -- !!!! This doesn't appear to be safe across save/load cycles ????
        return
    end

    -- Go through all hexes the unit can reach
    for i,r in ipairs(reach) do

        -- only consider unoccupied hexes 
        local occ_hex = wesnoth.get_units { x=r[1], y=r[2], { "not", { id = unit.id } } }[1]
        if not occ_hex then
            -- Find combined distance weighting of all enemy units within radius
            local value = 0
            for j,e in ipairs(enemies) do
                local d = H.distance_between(r[1], r[2], e.x, e.y)
                value = value + 1/ d^2
            end
            --wesnoth.fire("label", {x=r[1], y=r[2], text = math.floor(value*1000) } )

            -- Store this weighting in the third field of each 'reach' element
            reach[i][3] = value
        else
            reach[i][3] = 9999
        end
    end

    -- Sort 'reach' by values, smallest first
    table.sort(reach, function(a, b) return a[3] < b[3] end )
    -- Select those within factor 2 of the minimum
    local best_pos = self:filter(reach, function(tmp) return tmp[3] < reach[1][3]*2 end)

    -- Now take 'seek' and 'avoid' into account
    for i,b in ipairs(best_pos) do

        -- weighting based on distance from 'seek' and 'avoid'
        local ds = self:generalized_distance(b[1], b[2], tonumber(seek_x), tonumber(seek_y))
        local da = self:generalized_distance(b[1], b[2], tonumber(avoid_x), tonumber(avoid_y))
        --items.place_image(b[1], b[2], "items/ring-red.png")
        local value = 1 / (ds+1) - 1 / (da+1)^2 * 0.75

        --wesnoth.fire("label", {x=b[1], y=b[2], text = math.floor(value*1000) } )
        best_pos[i][3] = value
    end

    -- Sort 'best_pos" by value, largest first
    table.sort(best_pos, function(a, b) return a[3] > b[3] end)
    -- and select all those that have the maximum score
    local best_overall = self:filter(best_pos, function(tmp) return tmp[3] == best_pos[1][3] end)

    -- As final step, if there are more than one remaining locations,
    -- we take the one with the minimum score in the distance-from_enemy criterion
    local min, mx, my = 9999, 0, 0
    for i,b in ipairs(best_overall) do

        --items.place_image(b[1], b[2], "items/ring-white.png")
        local value = 0
        for j,e in ipairs(enemies) do
            local d = H.distance_between(b[1], b[2], e.x, e.y)
            value = value + 1/d^2
        end

        if value < min then
            min = value
            mx,my = b[1], b[2]
        end
    end
    --items.place_image(mx, my, "items/ring-gold.png")

    -- (mx,my) is the position to move to
    if (mx ~= unit.x or my ~= unit.y) then
        ai.move(unit, mx, my)
    end
    ai.stopunit_all(unit)    -- !!!! This doesn't appear to be safe across save/load cycles ????
end

return coward_ai
--! ==============================================================
>>
        [/engine]
    [/ai]
#enddef

#define COWARD_UNIT SIDE ID RADIUS SEEK_X SEEK_Y AVOID_X AVOID_Y
    [add_ai_behavior]
        side={SIDE}
        [filter]
             id="{ID}"
        [/filter]
	    sticky=yes
	    loop_id=main_loop
        evaluation="return (...):coward_evaluation('{ID}')"
        execution="(...):coward_execution('{ID}', {RADIUS}, '{SEEK_X}', '{SEEK_Y}', '{AVOID_X}', '{AVOID_Y}')"
    [/add_ai_behavior]
#enddef
Last edited by mattsc on November 27th, 2011, 11:33 pm, edited 2 times in total.
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Question for the Lua experts: in the above code, there are two places where I have an ai.stopunit_all(unit) commented out. When I left it in, everything works fine if I start the scenario from the beginning, but after a reload, it hangs Wesnoth. So for some reason stopunit does not appear safe across save/load cycles? Is that likely a bug, a feature, or am I using it incorrectly? (This applies to the stopunit commands in both places and I also tried stopunit_moves and stopunit_attacks with the same result.)

The replacement by manually removing the moves and attacks works just fine, except that the AI will still try to move the units to unoccupied villages (which will fail, of course, since there are no moves left). Not sure why this happens, my best guess is that the "capture village" candidate action looks for some unit characteristic other than the moves variable to determine its actions. Not sure what that would be though. As I said, the overall behavior is as desired just that one gets an 'E_EMPTY_MOVE' error message in the terminal window.
Post Reply