Another basic lua query
Moderator: Forum Moderators
- Spannerbag
- Posts: 787
- Joined: December 18th, 2016, 6:14 pm
- Location: Yes
Re: Another basic lua query
Good to know unit context of __cfg, thanks.

When the ***** documentation is helpful...

Thanks for the clarification!
Cheers!
-- Spannerbag
- Spannerbag
- Posts: 787
- Joined: December 18th, 2016, 6:14 pm
- Location: Yes
Re: Another basic lua query: syntax query #2
gnombat wrote: ↑July 2nd, 2025, 2:54 pm Try this:
Code: Select all
local my_filter = { side = wesnoth.current.side, { "filter_adjacent", { id = target.id } } } my_filter = wml.merge(filter_follower, my_filter, 'append') wesnoth.interface.add_chat_message(wml.tostring(my_filter))

Faffed around for awhile trying to understand why variables were correctly interpreted sometimes but not always.
Don't fully know why but am 99% certain it's my lack of familiarity with lua

Anyway got the following to run without errors -

It even seemed to produce the desired behaviour!

Code: Select all
local filter_follower = wml.get_child(cfg, "filter_follower")
-- Debug
local my_filter = wml.merge(filter_follower, { side = wesnoth.current.side, wml.tag.filter_adjacent { id = target.id } } )
wesnoth.interface.add_chat_message(wml.tostring(my_filter))
local adj_followers = wesnoth.units.find_on_map ( wml.merge(filter_follower, { side = wesnoth.current.side, wml.tag.filter_adjacent { id = target.id } } ) )
append
however it was good to demonstrate that syntax as I'd struggles with that, too. 
With
append
:
Without
Whilst it probably wasn't worth all this effort just to remove a few and
clauses, I've learned a bit more about lua! Thanks to everyone who took the trouble to reply, your time and trouble are greatly appreciated.
Cheers!
-- Spannerbag
- Spannerbag
- Posts: 787
- Joined: December 18th, 2016, 6:14 pm
- Location: Yes
Re: Another basic lua query: sanity check
Hi,
I'm puzzled (again) and would appreciate someone sanity checking my code to narrow down where the issue might be. Don't think the same follower should be processed twice but maybe that's a result of this error (line 41)?
I originally used
The chat works fine so
Digging into ai_helper only consufed me more; code around line 339:
So, I'm rather baffled and before I start scattering code into a local copy of ai_helper I'd appreciate any ideas from wiser heads.
If it helps I can publish a test scenario so the code can be examined in situ if anyone's that mad keen enough.
If it helps, here's the current follower move ca in full (based on
As always, thanks for your continuing patience and assistance!
Cheers,
-- Spannerbag
I'm puzzled (again) and would appreciate someone sanity checking my code to narrow down where the issue might be. Don't think the same follower should be processed twice but maybe that's a result of this error (line 41)?
Code: Select all
function ca_wanderers_follower_move:evaluation(cfg)
local follower = get_followers(cfg)[1]
-- debug
if follower then
wesnoth.interface.add_chat_message(string.format("Follower move this follower type=%s x,y=%d,%d", follower.type, follower.x, follower.y))
end
if (not cfg.follower_attack and follower) then
AH.checked_stopunit_attacks(ai, follower) >>>>>>>>>> Line 41 <<<<<<<<<<
end -- By default followers do not attack
if (not get_followers(cfg)[1]) then return 0 end -- No followers with moves found
if (not get_wanderers(cfg)[1]) then return 0 end -- No wanderers on map match filter
return cfg.ca_score
end
get_followers(cfg)[1]
but assigned follower
to make debugging less verbose.The chat works fine so
follower
contains data (at that point).Digging into ai_helper only consufed me more; code around line 339:
Code: Select all
function ai_helper.checked_stopunit_attacks(ai, unit)
local check = ai.check_stopunit(unit)
if (not check.ok) then
ai_helper.checked_action_error('ai.stopunit_attacks of ' .. unit.x .. ',' .. unit.y, check.status .. ' (' .. check.result .. ')')
return check
end
return ai.stopunit_attacks(unit) >>>>>>>>>> Line 339 <<<<<<<<<<
end
ai.check_stopunit(unit)
didn't complain (maybe it's OK with nil values?) but check.ok
is true as I don't see any message so I presume unit
was valid at that point?So, I'm rather baffled and before I start scattering code into a local copy of ai_helper I'd appreciate any ideas from wiser heads.
If it helps I can publish a test scenario so the code can be examined in situ if anyone's that mad keen enough.
If it helps, here's the current follower move ca in full (based on
ca_forest_animals_tusklet_move.lua
).
Code: Select all
-- ca_wanderers_follower_move.lua
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local M = wesnoth.map
local function get_followers(cfg)
local filter_follower = wml.get_child(cfg,"filter_follower")
if (not filter_follower) then
return {}
else
local followers = AH.get_units_with_moves { side = wesnoth.current.side, { "and", filter_follower } }
return followers
end
end
local function get_wanderers(cfg)
local filter_wanderer = wml.get_child(cfg,"filter_wanderer")
if (not filter_wanderer) then
return {}
else
local wanderers = AH.get_units_with_moves ( wml.merge ( { side = wesnoth.current.side }, filter_wanderer ) )
return wanderers
end
end
local ca_wanderers_follower_move = {}
-- Followers move toward closest wanderer (if any) otherwise already made random move (ca_wanderers_move)
function ca_wanderers_follower_move:evaluation(cfg)
local follower = get_followers(cfg)[1]
-- debug
if follower then
wesnoth.interface.add_chat_message(string.format("Follower move this follower type=%s x,y=%d,%d", follower.type, follower.x, follower.y))
end
if (not cfg.follower_attack and follower) then
AH.checked_stopunit_attacks(ai, follower)
end -- By default followers do not attack
if (not get_followers(cfg)[1]) then return 0 end -- No followers with moves found
if (not get_wanderers(cfg)[1]) then return 0 end -- No wanderers on map match filter
return cfg.ca_score
end
function ca_wanderers_follower_move:execution(cfg)
local follower = get_followers(cfg)[1] -- Follower being moved
local wanderers = get_wanderers(cfg) -- All "followable" units
local avoid_locs = AH.get_avoid_map(ai, nil, true) -- [avoid]ed locations, if any
local min_dist, goto_wanderer = math.huge, nil
for _,wanderer in ipairs(wanderers) do
local dist = M.distance_between(wanderer.x, wanderer.y, follower.x, follower.y)
if (dist < min_dist) then min_dist, goto_wanderer = dist, wanderer end
end
local best_hex = AH.find_best_move(follower, function(x, y)
return - M.distance_between(x, y, goto_wanderer.x, goto_wanderer.y) -- Definition of anonymous function (x, y), see ai_helper.lua line 1996
end, { avoid_map = avoid_locs }) -- Honour [avoid] (hopefully) when selecting best move
wesnoth.interface.add_chat_message(string.format("ca_wanderers_follower_move: movefull_stopunit on %s at %d,%d.", follower.type, follower.x, follower.y))
AH.movefull_stopunit(ai, follower, best_hex)
if follower and follower.valid then
wesnoth.interface.add_chat_message(string.format("ca_wanderers_follower_move: stopunit_all on %s at %d,%d.", follower.type, follower.x, follower.y))
AH.checked_stopunit_all(ai, follower) end -- Followers never attack
end
return ca_wanderers_follower_move
Cheers,
-- Spannerbag
Re: Another basic lua query
According to the documentation:
(It would be nice if it gave a better error message in this situation.)
You are calling the functionhttps://wiki.wesnoth.org/LuaAPI/ai#ai.read_only wrote: The AI module operates in two modes: read-only, and read-write. Read-write mode is used in the execution function for stages and candidate actions, while read-only mode is used in goals and aspects and in the evaluation function for candidate actions... Functions that are only available in read-write mode will be tagged (mutable) in the descriptions on this page.
ai.stopunit_attacks
(indirectly) from the evaluation function. But this won't work because it is documented as "mutable".(It would be nice if it gave a better error message in this situation.)
- Spannerbag
- Posts: 787
- Joined: December 18th, 2016, 6:14 pm
- Location: Yes
Re: Another basic lua query
gnombat wrote: ↑July 4th, 2025, 12:17 pm ...You are calling the functionai.stopunit_attacks
(indirectly) from the evaluation function. But this won't work because it is documented as "mutable".
(It would be nice if it gave a better error message in this situation.)
Ah, thanks - I have a hard time finding the relevant parts of the documentation sometimes.

I did search for
lua
and (various sombinations of) execution
and evaluation
but didn't find anything helpful or enlightening so assumed that they weren't special functions, but rather a standard naming convention. 
That paragraph explains a few things - hope I remember them...
Many thanks as ever for your help, greatly appreciated.
Cheers!
-- Spannerbag
Re: Another basic lua query
They are documented in general terms (not Lua-specific) here: https://wiki.wesnoth.org/Wesnoth_AI_Fra ... loop_StageSpannerbag wrote: ↑July 4th, 2025, 2:13 pm Ah, thanks - I have a hard time finding the relevant parts of the documentation sometimes.
I did search forlua
and (various sombinations of)execution
andevaluation
but didn't find anything helpful or enlightening so assumed that they weren't special functions, but rather a standard naming convention.![]()
They are documented specifically for Lua here: https://wiki.wesnoth.org/Creating_Custo ... _Custom_AI
The Lua documentation is somewhat confusing because it's using pre-1.14 code which is a little bit different - however, probably 99% of the documentation is still relevant.
The main thing to note is that the evaluation function isn't actually supposed to "do" anything - it is just supposed to return a score. So it should not be calling things like
ai.stopunit_attacks
. (However, the evaluation function can store data in variables if you need that.) The execution function is where actual actions should take place.- Spannerbag
- Posts: 787
- Joined: December 18th, 2016, 6:14 pm
- Location: Yes
Re: Another basic lua query
Thanks for that - forgot to log out earlier due to rl.gnombat wrote: ↑July 4th, 2025, 3:21 pmThey are documented in general terms (not Lua-specific) here: https://wiki.wesnoth.org/Wesnoth_AI_Fra ... loop_StageSpannerbag wrote: ↑July 4th, 2025, 2:13 pm Ah, thanks - I have a hard time finding the relevant parts of the documentation sometimes.
I did search forlua
and (various sombinations of)execution
andevaluation
but didn't find anything helpful or enlightening so assumed that they weren't special functions, but rather a standard naming convention.![]()
They are documented specifically for Lua here: https://wiki.wesnoth.org/Creating_Custo ... _Custom_AI
The Lua documentation is somewhat confusing because it's using pre-1.14 code which is a little bit different - however, probably 99% of the documentation is still relevant.
The main thing to note is that the evaluation function isn't actually supposed to "do" anything - it is just supposed to return a score. So it should not be calling things likeai.stopunit_attacks
. (However, the evaluation function can store data in variables if you need that.) The execution function is where actual actions should take place.

The reason why I was doing what I was doing was to set
attacks=0
for all followers regardless of movement (or lack thereof) if the key follower_attack
is false (the default).Haven't read your links yet (many thanks BTW, I will read when I get chance, probably this weekend) but was concerned that if the evaluation resulted in "do nothing" (return 0) then the evaluation logic wouldn't execute.
I suppose I'm trying to do something that isn't really a "ca action" as it would be easy to do this in an
[event]
.Perhaps I'm doing this in the wrong place/way; maybe the best design approach is to make the micro ai as simple as possible...
I was going to experiment with putting the
attacks=0
logic in the main definition file mai_wanderers.lua
.However I have stumbled across some other weirdness that I'll need to spend (yet more

Thanks ever so much for your help, really appreciated.

Cheers!
-- Spannerbag
Re: Another basic lua query
You mean the execution logic wouldn't execute?Spannerbag wrote: ↑July 4th, 2025, 5:24 pm The reason why I was doing what I was doing was to setattacks=0
for all followers regardless of movement (or lack thereof) if the keyfollower_attack
is false (the default).
Haven't read your links yet (many thanks BTW, I will read when I get chance, probably this weekend) but was concerned that if the evaluation resulted in "do nothing" (return 0) then the evaluation logic wouldn't execute.
I would recommend looking at the
ca_return_guardian
example described in https://wiki.wesnoth.org/Creating_Custo ... _Custom_AI - it seems that what it is doing is analogous to what you want to do and it discusses the logic in detail. Basically the ca_return_guardian
is trying to do this:- It wants to stop the "guardian" units from wandering all over the map
- It returns a nonzero score from the
evaluation
function - in particular it returns a higher score than the "move to targets" CA (so thatca_return_guardian:execution
will run before "move to targets" runs) - Then in the
ca_return_guardian:execution
function it callsai.stopunit_moves
to remove the movement of the guardian unit (before it has a chance to move)
- You want to stop the "follower" units from attacking
- You should return a nonzero score from the
evaluation
function - in particular you should return a higher score than the "combat" (attack) CA - Then in the
ca_return_guardian:execution
function you should callai.stopunit_attacks
to remove the attacks of the follower unit (before it has a chance to attack)
- Spannerbag
- Posts: 787
- Joined: December 18th, 2016, 6:14 pm
- Location: Yes
Re: Another basic lua query
Yep...gnombat wrote: ↑July 4th, 2025, 6:08 pmYou mean the execution logic wouldn't execute?Spannerbag wrote: ↑July 4th, 2025, 5:24 pm ...concerned that if the evaluation resulted in "do nothing" (return 0) then the evaluation logic wouldn't execute.

Many thanks for another enlightening reply; have read the various links and I've learned a bit more.gnombat wrote: ↑July 4th, 2025, 6:08 pm I would recommend looking at theca_return_guardian
example described in https://wiki.wesnoth.org/...

Will put all this together, recode, test and continue making incremental gains as my understanding slowly develops.
Your time and trouble are greatly appreciated.

Cheers!
-- Spannerbag
- Spannerbag
- Posts: 787
- Joined: December 18th, 2016, 6:14 pm
- Location: Yes
Re: Another basic lua query: optimisation query
Hi,
After some thought decided to implement disabling follower attacks (the default) as a separate ca (
This works
but is inefficient.
I understand what is happening and how to fix it but just wanted to ask if there was a standard or easy way to fix before using my home brewed methods.
If this involves redesigning the logic, that's OK.
Here's the ca setup; last line of snippet below (returns the highest score of all cas in this micro ai):
Happy to post the ca code if required.
I had hoped that the code would only execute once but as the evaluation doesn't check if it's already run (on the current turn) and the logic doesn't change the conditions the ca tests, I can (now, with hindsight) see it will run multiple times.
I wanted to process all followers regardless of moves etc. because the other cas check for units with moves etc.
Whilst this is probably okay I thought a separate ca might be preferable as explained previously.
In the absence of anything better, I'll probably either;
As ever thanks for your time and trouble.
Cheers!
-- Spannerbag
After some thought decided to implement disabling follower attacks (the default) as a separate ca (
ca_wanderers_follower_attack
), mainly for clarity and as a learning exercise (the ca scores returned by other cas are sufficiently high to do the job).This works



I understand what is happening and how to fix it but just wanted to ask if there was a standard or easy way to fix before using my home brewed methods.
If this involves redesigning the logic, that's OK.

Here's the ca setup; last line of snippet below (returns the highest score of all cas in this micro ai):
Code: Select all
local score = cfg.ca_score or 300000
local CA_parms = {
ai_id = 'mai_wanderers',
{ ca_id = "follower_attack", location = '~add-ons/stub18/ai/ca_wanderers_follower_attack.lua', score = score + 1 },
I had hoped that the code would only execute once but as the evaluation doesn't check if it's already run (on the current turn) and the logic doesn't change the conditions the ca tests, I can (now, with hindsight) see it will run multiple times.

I wanted to process all followers regardless of moves etc. because the other cas check for units with moves etc.
Whilst this is probably okay I thought a separate ca might be preferable as explained previously.
In the absence of anything better, I'll probably either;
- Revert to my previous versions and embed the attack removal into other cas or,
- Tack a flag value onto
cfg
- assuming it wouldn't break anything else?
Edit: realised this probably won't work because (I assume)cfg
is loaded from (currently static) WML every time...
As ever thanks for your time and trouble.
Cheers!
-- Spannerbag
Re: Another basic lua query: optimisation query
I'm not sure I understand exactly what you're attempting here, but possibly you want to callSpannerbag wrote: ↑Yesterday, 11:43 am I had hoped that the code would only execute once but as the evaluation doesn't check if it's already run (on the current turn) and the logic doesn't change the conditions the ca tests, I can (now, with hindsight) see it will run multiple times.![]()
get_units_with_attacks
in the evaluation function, and then if there are none, return a score of 0. There are several CAs which do this; for example, ca_simple_attack.lua.- Spannerbag
- Posts: 787
- Joined: December 18th, 2016, 6:14 pm
- Location: Yes
Re: Another basic lua query: optimisation query
D'Oh! - I should've thought of that!



Thanks for pointing out the obvious, much simpler than my baroque solutions.
You saved me a lot of hassle, very much appreciated!
Update: only runs once per turn now - thanks again!

-- Spannerbag