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 »

Well it seems the global lua variable ai doesn't get redefined upon reload. It makes sense since you have the assignment ai = ... in [side] which is only read at scenario start. The lua state is destroyed upon scenario ending (advancing to the next scenario, loading a saved game, "back to turn x" etc) and it takes all variables with it if they haven't been put into wml variables. Whether this is a bug or intended behavior I dunno in this case. You get a similar effect when defining some function in a preload event with first_time_only=yes.
A way to solve it could probably be to store the complete ai table into a wml variable on saving (wesnoth.game_events.on_save) and to restore it when loading (wesnoth.game_events.on_load) in case that the ai table is still empty. In case that it is a bug this could be a workaround.
Do you code anything for this ai table or is it pre-created by the game ? In the ladder case I vote for a bug.
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 »

I'm not sure that that's it. It's my understanding that the ai table is created by the engine at the beginning of the AI's turn (that's what the '...' is about, I think). It contains all the Lua AI functions/actions and only stopunit seems to be causing problems. There's an ai.move() 2 lines before the second stopunit line that doesn't make any trouble, and I've use ai.attack() without trouble before also.

... and since I wrote that I did some more tests and figured out what is going on, although I do not know why. The AI got stuck in an infinite loop. From what I understand, there are 3 reasons why a candidate action is not called any more:
1. Its evaluation returns a value <=0: that's not the case here since I had it hard coded to return 300,000.
2. The CA tries to perform an invalid move (e.g. moving a unit with no moves left): it gets black listed for the remainder of the turn
3. If a CA gets called too many times without changing the game state, the loop gets broken after a certain number of iterations (not sure about this one one, I think I read that somewhere, but don't remember where).

So, 1. and 2. don't apply here, I think. 1. for the mentioned reasons, 2. because (apparently?) a stopunit action does not count as an invalid move even if the unit has no actions left (which makes sense). If that's true, that means that 3, for some reason, kicks in before a reload of the scenario, but not after. That sounds like a bug to me (assuming I have it right), I just don't know if the before or the after is the bug... I'll put that on my list of things to talk to Crab about.

Anyways, I have now changed the evaluation routine to return a valid score only when the unit has moves left, and that takes care of the problem. It's a cleaner way of doing it anyway. I want to do a bit more testing, then I'll update the code posted earlier.

As for your other question, I used to store my helper functions inside the ai variable, but since that holds all the default engine functions, I decided that that was probably bad practice and am not doing it any more. That's what I am using ai_helper for now (or I am defining them directly in the engine, as in the posted examples).
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Further on that topic: the global variable ca_counter (candidate action counter?) defined in the preload event has value 2 inside coward_ai:coward_execution() when the scenario is started from the beginning. It has value 0 after a reload. It seems that the ai function table gets set correctly again after a reload (at the beginning of the AI turn), but that the counter variable is not set. I don't know if that is related to the above problem, but it's currently my best guess.

[I updated the code posted earlier. It now works after reloading too.]

EDIT: The only place where I can find ca_counter in either the source or the lua code is in the definition of [add_ai_behavior], and there it is only used to set name and id of the new behavior candidate action (BCA). So I thought it might cause problems if adding BCA's both before and after a reload, as we then might get different CA's with the same name/id, but that doesn't seem to cause any problems, at least not in the tests I did. So, I don't think that this is the cause of the problem -- and I am wondering if there really is a need for having to define ca_counter in the first place in a preload event (it just seems "unelegant") or whether this shouldn't be handled differently in the add_ai_behavior code (which is written in Lua and easily modified).
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Simons: Below are the macros for the stationed guardian. The macro used for a guardian unit is defined like this:
#define STATIONED_GUARDIAN_UNIT SIDE ID RADIUS STATION_X STATION_Y GUARD_X GUARD_Y
'SIDE' is the side of the guardian unit (needed so that the behavior can be applied to the correct side's AI), 'ID' its id. 'RADIUS' is the distance over which the actions take effect (as we discussed, I implemented that as distance in hexes, rather than movement cost). Then come the cordinates of the station and the guarded location.

The behavior is as follows:
- If no enemy is within radius of the guard's current position: do nothing
- Otherwise: If an enemy is within 'radius' of the guard, but not also within the same distance of the guarded location and the station (all of this simultaneously), move the guard in the direction of the station
- Otherwise:
- Pick the enemy that is closest to the guarded location
- If we can reach him, pick the adjacent hex with the highest defense rating and attack from there
- If not in reach, move toward this unit

I think this is pretty much as you described, but if not, it's easy to change any of that. As for the choice of unit to be attacked, I figured that the highest priority target for our guard would be the unit that is closest to the position he guards. Note, however, that this means that the guard always moves toward that closest unit (even if it is out of his current MP reach, because of terrain or enemy ZoC), and might skip a unit that he can reach but that is not as close to the guarded location. Still, I think that makes sense given the priorities I set above. If, on this move, the guard ends up at a position next to an enemy, he'll still attack that enemy, even if it was not the original target.

You can also get somewhat strange behavior if you choose radius much smaller or larger than the units MP and the distance between the two locations, but that's up to the scenario designer to set up in a sensible way. Also, those strange behaviors might be desirable in some cases.

One more thing: you mentioned shroud before, but as the standard Wesnoth AI behavior is to ignore shroud and fog, it is also not taken into account here. If you want me to add that, I'm sure that can be done. I'll have to look up how exactly, but the functionality is there.

A couple technical notes for those interested in the code:
- The attack code can be simplified quite a bit once ai.get_attacks() has been implemented in Lua AI.
- The evaluation value is set so that this action happens just before the normal combat candidate action
- If a the guard moves toward the station, or an enemy without getting there, but ends up next to another enemy, it will still attack this through the standard RCA AI actions. This is done by only using ai.stopunit_moves() at the end, not ai.stopunit_all()

Finally, here's the code. It has the same 3 macros as the previous examples: {STATIONED_GUARDIAN_PRELOAD} (put inside scenario tags), {STATIONED_GUARDIAN_ENGINE} (inside the side definition) and {STATIONED_GUARDIAN_UNIT SIDE ID RADIUS STATION_X STATION_Y GUARD_X GUARD_Y} (inside action WML after the unit has been created). Other units of the side will behave as usual.

EDIT: renamed the preload macro STATIONED_GUARDIAN_PRELOAD, to make it consistent with the other examples. The default LUA_AI_PRLEOAD can also be used, but might contain parts not necessary for the stationed guardian.

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

Code: Select all

#define STATIONED_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 STATIONED_GUARDIAN_ENGINE
    {ai/aliases/stable_singleplayer.cfg}
    [ai]
        [engine]
            name="lua"
            code= <<
--! ==============================================================
local ai = ...

local stationed_guardian_ai = {}

function stationed_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 stationed_guardian_ai:guardian_evaluation(id)

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

    if (unit.moves > 0) then
        value = 100010
    else
        value = 0
    end

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

function stationed_guardian_ai:guardian_execution(id, radius, s_x, s_y, g_x, g_y)
    -- (s_x,s_y): coordinates where unit is stationed; tries to move here if there is nobody to attack
    -- (g_x,g_y): location that the unit guards

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

    -- find if there are enemies within 'radius'
    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 'radius': keep unit from doing anything and exit
    if not enemies[1] then 
        --print("No enemies close -> sleeping:",unit.id)
        ai.stopunit_moves(unit)
        return
    end

    -- Otherwise, unit will either attack or move toward station
    --print("Guardian unit waking up",unit.id)

    -- enemies must be within 'radius' of guard, (s_x,s_y) *and* (g_x,g_y)
    -- simultaneous for guard to attack
    local target = {}
    local min_dist = 9999
    for i,e in ipairs(enemies) do
        local ds = H.distance_between(s_x, s_y, e.x, e.y)
        local dg = H.distance_between(g_x, g_y, e.x, e.y)

        -- If valid target found, save the one with the shortest distance from (g_x,g_y)
        if (ds <= radius) and (dg <= radius) and (dg < min_dist) then 
            --print("target:", e.id, ds, dg)
            target = e
            min_dist = dg
        end
    end

    -- If a valid target was found, unit attacks this target, or moves toward it
    if (min_dist ~= 9999) then
        --print ("Go for enemy unit:", target.id)

        -- Find tiles adjacent to the target, and save the one that our unit 
        -- can reach with the highest defense rating
        local best_defense = -9999
        local attack_loc = 0
        for x,y in H.adjacent_tiles(target.x, target.y) do
            -- only consider unoccupied hexes 
            local occ_hex = wesnoth.get_units { x=x, y=y, { "not", { id = unit.id } } }[1]
            if not occ_hex then
                -- defense rating of the hex
                local defense = 100 - wesnoth.unit_defense(unit, wesnoth.get_terrain(x, y))
                --print(x,y,defense)
                local nh = self:next_hop(unit, x, y)
                -- if this is best defense rating and unit can reach it, save this location
                if (nh[1] == x) and (nh[2] == y) and (defense > best_defense) then
                    attack_loc = {x, y}
                    best_defense = defense
                end
            end
        end

        -- If a valid hex was found: move there and attack
        if (best_defense ~= -9999) then
            --print("Attack at:",attack_loc[1],attack_loc[2],best_defense)
            ai.move(unit, attack_loc[1],attack_loc[2])
            ai.attack(unit, target)
        else  -- otherwise move toward that enemy
            --print("Cannot reach target, moving toward it")
            local reach = wesnoth.find_reach(unit)

            -- Go through all hexes the unit can reach, find closest to target
            local nh = {}  -- cannot use next_hop here since target hex is occupied by enemy
            local min_dist = 9999
            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
                    local d = H.distance_between(r[1], r[2], target.x, target.y)
                    if d < min_dist then 
                        min_dist = d
                        nh = {r[1], r[2]}
                    end
                end
            end

            -- Finally, execute the move toward the target
            if (nh[1] ~= unit.x) or (nh[2] ~= unit.y) then 
                ai.move_full(unit, nh[1], nh[2])
            end
        end

    -- If no enemy within the target zone, move toward station position
    else
        --print "Move toward station"
        local nh = self:next_hop(unit, s_x, s_y)
        if (nh[1] ~= unit.x) or (nh[2] ~= unit.y) then 
            ai.move_full(unit, nh[1], nh[2])
        end
    end

    ai.stopunit_moves(unit)  -- just in case we did not use up all MP
    -- If there are attacks left and unit ended up next to an enemy, we'll leave this to RCA AI
end

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

#define STATIONED_GUARDIAN_UNIT SIDE ID RADIUS STATION_X STATION_Y GUARD_X GUARD_Y
    [add_ai_behavior]
        side={SIDE}
        [filter]
             id="{ID}"
        [/filter]
	    sticky=yes
	    loop_id=main_loop
        evaluation="return (...):guardian_evaluation('{ID}')"
        execution="(...):guardian_execution('{ID}', {RADIUS}, {STATION_X}, {STATION_Y}, {GUARD_X}, {GUARD_Y})"
    [/add_ai_behavior]
#enddef
Last edited by mattsc on November 27th, 2011, 11:33 pm, edited 2 times 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 »

Well, I plumbed in a couple of stationed guards into my current working scenario, and the work fine. My first ever guards who return to their stations!

/me color="#impressed"

(Which is just black, apparently.)
 
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

I have a suggestion for changing the code of the [add_ai_behavior] tag, which is written in Lua (in 'lua/wml-tags.lua'). How do I go about making that an official request? It's neither really a bug nor a feature request, although it could be considered the latter, I guess. (And yes, I know that we're in a feature freeze right now, so there's plenty of time for this.)

In brief: [add_ai_behavior] requires the global variable 'ca_counter' to be set, which is, I believe, unnecessary with a minor(ish) change to the code. If it is not set, Wesnoth crashes when the tag is called. Furthermore, I believe that even if an engine other than Lua AI is used, the variable needs to be set as a global Lua variable [untested]. Also, as I have mentioned before, 'ca_counter' is not preserved after reload, which doesn't matter as long as we only add candidate actions, but can cause trouble if we want to remove them manually later.

I am pretty sure that I know how to make the needed change to the code.
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

I have set up a few wiki pages describing how to modify the Wesnoth AI using both Formula AI and Lua AI. They are meant to complement the information on the existing wiki pages, not replace it. I wrote them in a howto-ish, tutorial-kind-of-style, with a specific emphasis on setting up an efficient development and testing environment. The front page can be found here: Practical Guide to Modifying AI Behavior.

If this is found to be useful, I could link to it from the existing pages, probably from the beginning of Customizing AI in Wesnoth 1.8.

The 'Example Code' pages are not done yet, containing only placeholder lists that are not yet complete (mostly they are just the examples from this thread). Also, while I was putting these pages together, I found a number of inconsistencies on the existing wiki pages. My guess is that they mostly stem from earlier incarnations of the AIs that have by now been changed. I assume it is ok if I go ahead and fix those?

Anyways, hope this is useful to at least one or two of you!
Max
Posts: 1449
Joined: April 13th, 2008, 12:41 am

Re: Exercises in Formula and Lua AI

Post by Max »

wow - that's a really valuable contribution, thanks!
User avatar
Coffee
Inactive Developer
Posts: 180
Joined: October 12th, 2010, 8:24 pm

Re: Exercises in Formula and Lua AI

Post by Coffee »

Thanks for setting up the Wiki page mattsc. I have a bunch of example FAIs that might be applicable that I've been working on recently.

Also, I've been using [modify_ai] tags in [side] WML areas for my campaign. This might be useful to add that you can just add to the RCA AI without specifying a version number or engine.

I really like the idea of getting a page up that shows how to use FAI and LUA AI easily with examples. I'd be glad to help you with this.
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Coffee:
Coffee wrote:Thanks for setting up the Wiki page mattsc. I have a bunch of example FAIs that might be applicable that I've been working on recently.
...
I really like the idea of getting a page up that shows how to use FAI and LUA AI easily with examples. I'd be glad to help you with this.
That would be great, especially since I have mostly LAI and very few FAI examples. I set up the beginning of the FAI example page, with the one example I have (I also have a bunch of working code on alternative attack strategies etc., but none of them are ready to post). If you could add to that, that would be great. Or if you prefer me to put them up, send them to me and I'll do it.
Coffee wrote:Also, I've been using [modify_ai] tags in [side] WML areas for my campaign. This might be useful to add that you can just add to the RCA AI without specifying a version number or engine.
Sounds good. I haven't tried that, all the examples I tried need the version number. I will see where to add this in the pages. Just to confirm first though, you are doing that without having an {ai/aliases/stable_singleplayer.cfg} (which contains the version number line) or equivalent in your scenario somewhere?
User avatar
Coffee
Inactive Developer
Posts: 180
Joined: October 12th, 2010, 8:24 pm

Re: Exercises in Formula and Lua AI

Post by Coffee »

mattsc wrote:Just to confirm first though, you are doing that without having an {ai/aliases/stable_singleplayer.cfg} (which contains the version number line) or equivalent in your scenario somewhere?
Yeah, the 2 examples for Formula AI from my campaign demonstrate how to do it with just [Modify_AI] WML macros from AI.cfg. I think this makes the most sense to do since the version numbers change.

Just wondering if this is possible to do in LUA in a similar way?
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Coffee wrote:Yeah, the 2 examples for Formula AI from my campaign demonstrate how to do it with just [Modify_AI] WML macros from AI.cfg. I think this makes the most sense to do since the version numbers change.
I saw that. Thanks for posting the examples! I will add that method to the wiki page.
Coffee wrote:Just wondering if this is possible to do in LUA in a similar way?
To be honest, I don't know. For all I know you always need to define an engine for Lua AI, but I have not tried only adding a self-contained candidate action. I will test that and report back (and change the wiki if it works).
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Coffee wrote:Yeah, the 2 examples for Formula AI from my campaign demonstrate how to do it with just [Modify_AI] WML macros from AI.cfg. I think this makes the most sense to do since the version numbers change.

Just wondering if this is possible to do in LUA in a similar way?
I confirmed that this works in Formula AI, but not in Lua AI. In fact, in Lua not only do we need to set up the engine, but the main_loop stage needs to be defined explicitly also, even if it does not contain any CAs.

I added a section to the FAI Howto page on how to do this, and a comment to the LAI Howto page that it doesn't work there.
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 »

I've plugged in the coward AI now, and I am pleased to announce I now have hordes -or herds- of cowardly horses.

But this seems to have uncovered a bug. My horses have defined flee_to locations, as you might expect, and when they reach that hex, or ones near to it, they are killed to remove them. This upsets the lua_ai which sometimes seems to want to move them further. Result is a lua error:

Code: Select all

131: bad argument to stopunit_all
location (unit/integer) expected, got userdata
followed by a stacktrace

One trivial change I made, which I don't think should have upset anything, was to tidy the macro names slightly. I now have STATIONED_GUARDIAN_PRELOAD, STATIONED_GUARDIAN_ENGINE, STATIONED_GUARDIAN, COWARD_PRELOAD, COWARD_ENGINE and COWARD as my macro names. That alternation's harmless, isn't it?
 
mattsc
Inactive Developer
Posts: 1217
Joined: October 13th, 2010, 6:14 pm

Re: Exercises in Formula and Lua AI

Post by mattsc »

Simons: Yes, that change is harmless. Here's what's happening: the coward move execution has two separate 'AI moves' in it, the actual move away from the enemies, and then an 'ai.stopunit_all()' that takes all moves and attacks away from the unit at the very end of the code. The latter is supposed to kick in whether the unit moved away from somebody or not, and makes sure that it does not participate in any of the other (default) AI moves.

What happens in your case is that the unit gets killed after the move (through a 'moveto' event) and then isn't there any more for the stopunit command, which causes the error. There are 3 ways (at least) to take care of that:

- Program the code so that it only ever executes one action. That might not be possible in all cases, as sometimes we want a unit to do a move and an attack; or to do a move and disable all attacks; etc.
- Every AI move should probably be preceded by a 'ai.check_whatevermove_action()' call, just so that one is certain that one doesn't overlook something. Those functions aren't implemented in Lua AI yet, AFAIK, so that's not possible for now.
- For now, I simply check whether the unit still exists before the stopunit command. I tested it and I think it works.

I updated the macros here. The only change are 2 lines at the end of the engine macro. Could you test that that works now without producing error messages? Thanks for pointing this out!

I will go through the other examples and make sure similar things cannot happen for those also.
Post Reply