Another basic lua query

Discussion of Lua and LuaWML support, development, and ideas.

Moderator: Forum Moderators

User avatar
Spannerbag
Posts: 787
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Another basic lua query

Post by Spannerbag »

Ravana wrote: July 2nd, 2025, 2:32 pm ...__cfg of unit has specific meaning (similar to [store_unit])....
Good to know unit context of __cfg, thanks. :)

Ravana wrote: July 2nd, 2025, 2:32 pm ... __ attributes and functions are not implemented in Lua but anyways you need to check documentation.
When the ***** documentation is helpful... :augh:

Thanks for the clarification!

Cheers!
-- Spannerbag
SP Campaigns: After EI (v1.14) Leafsea Burning (v1.18, v1.16)
I suspect the universe is simpler than we think and stranger than we can know.
Also, I fear that beyond a certain point more intelligence does not necessarily benefit a species...
User avatar
Spannerbag
Posts: 787
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Another basic lua query: syntax query #2

Post by Spannerbag »

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))
8)

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 :doh:
Anyway got the following to run without errors - :D - and it returned expected/hoped for/correct values.
It even seemed to produce the desired behaviour! :shock:

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 } } ) )
 
Seems to produce identical outputs with or without append however it was good to demonstrate that syntax as I'd struggles with that, too. :augh:

With append:
wtest.jpg
wtest.jpg (123.46 KiB) Viewed 329 times
Without
wtestnoappend.jpg
wtestnoappend.jpg (107.28 KiB) Viewed 329 times
Whilst it probably wasn't worth all this effort just to remove a few and clauses, I've learned a bit more about lua! :thumbsup:

Thanks to everyone who took the trouble to reply, your time and trouble are greatly appreciated.

Cheers!
-- Spannerbag
SP Campaigns: After EI (v1.14) Leafsea Burning (v1.18, v1.16)
I suspect the universe is simpler than we think and stranger than we can know.
Also, I fear that beyond a certain point more intelligence does not necessarily benefit a species...
User avatar
Spannerbag
Posts: 787
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Another basic lua query: sanity check

Post by Spannerbag »

Hi,
I'm puzzled (again) and would appreciate someone sanity checking my code to narrow down where the issue might be.
wmaifm.jpg
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
I originally used 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
As always, thanks for your continuing patience and assistance!

Cheers,
-- Spannerbag
SP Campaigns: After EI (v1.14) Leafsea Burning (v1.18, v1.16)
I suspect the universe is simpler than we think and stranger than we can know.
Also, I fear that beyond a certain point more intelligence does not necessarily benefit a species...
gnombat
Posts: 903
Joined: June 10th, 2010, 8:49 pm

Re: Another basic lua query

Post by gnombat »

According to the documentation:
https://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.
You are calling the function 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.)
User avatar
Spannerbag
Posts: 787
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Another basic lua query

Post by Spannerbag »

gnombat wrote: July 4th, 2025, 12:17 pm ...You are calling the function 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.)

Ah, thanks - I have a hard time finding the relevant parts of the documentation sometimes. :doh:
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. :augh:

That paragraph explains a few things - hope I remember them...

Many thanks as ever for your help, greatly appreciated.

Cheers!
-- Spannerbag
SP Campaigns: After EI (v1.14) Leafsea Burning (v1.18, v1.16)
I suspect the universe is simpler than we think and stranger than we can know.
Also, I fear that beyond a certain point more intelligence does not necessarily benefit a species...
gnombat
Posts: 903
Joined: June 10th, 2010, 8:49 pm

Re: Another basic lua query

Post by gnombat »

Spannerbag wrote: July 4th, 2025, 2:13 pm Ah, thanks - I have a hard time finding the relevant parts of the documentation sometimes. :doh:
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. :augh:
They are documented in general terms (not Lua-specific) here: https://wiki.wesnoth.org/Wesnoth_AI_Fra ... loop_Stage

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.
User avatar
Spannerbag
Posts: 787
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Another basic lua query

Post by Spannerbag »

gnombat wrote: July 4th, 2025, 3:21 pm
Spannerbag wrote: July 4th, 2025, 2:13 pm Ah, thanks - I have a hard time finding the relevant parts of the documentation sometimes. :doh:
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. :augh:
They are documented in general terms (not Lua-specific) here: https://wiki.wesnoth.org/Wesnoth_AI_Fra ... loop_Stage

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.
Thanks for that - forgot to log out earlier due to rl. :doh:

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 :x ) time trying to chase down first.

Thanks ever so much for your help, really appreciated. :D

Cheers!
-- Spannerbag
SP Campaigns: After EI (v1.14) Leafsea Burning (v1.18, v1.16)
I suspect the universe is simpler than we think and stranger than we can know.
Also, I fear that beyond a certain point more intelligence does not necessarily benefit a species...
gnombat
Posts: 903
Joined: June 10th, 2010, 8:49 pm

Re: Another basic lua query

Post by gnombat »

Spannerbag wrote: July 4th, 2025, 5:24 pm 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.
You mean the execution 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:
  1. It wants to stop the "guardian" units from wandering all over the map
  2. It returns a nonzero score from the evaluation function - in particular it returns a higher score than the "move to targets" CA (so that ca_return_guardian:execution will run before "move to targets" runs)
  3. Then in the ca_return_guardian:execution function it calls ai.stopunit_moves to remove the movement of the guardian unit (before it has a chance to move)
It sounds like what you want to do is similar:
  1. You want to stop the "follower" units from attacking
  2. You should return a nonzero score from the evaluation function - in particular you should return a higher score than the "combat" (attack) CA
  3. Then in the ca_return_guardian:execution function you should call ai.stopunit_attacks to remove the attacks of the follower unit (before it has a chance to attack)
User avatar
Spannerbag
Posts: 787
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Another basic lua query

Post by Spannerbag »

gnombat wrote: July 4th, 2025, 6:08 pm
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.
You mean the execution logic wouldn't execute?
Yep... :doh:

gnombat wrote: July 4th, 2025, 6:08 pm I would recommend looking at the ca_return_guardian example described in https://wiki.wesnoth.org/...
Many thanks for another enlightening reply; have read the various links and I've learned a bit more. ^_^
Will put all this together, recode, test and continue making incremental gains as my understanding slowly develops.

Your time and trouble are greatly appreciated. :D

Cheers!
-- Spannerbag
SP Campaigns: After EI (v1.14) Leafsea Burning (v1.18, v1.16)
I suspect the universe is simpler than we think and stranger than we can know.
Also, I fear that beyond a certain point more intelligence does not necessarily benefit a species...
User avatar
Spannerbag
Posts: 787
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Another basic lua query: optimisation query

Post by Spannerbag »

Hi,
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 :shock: :D :shock: 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):

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 },
Happy to post the ca code if required.
wmai_opt.jpg
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. :doh:

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...
However if there's a better way to make this ca run just once per turn that would be great to know!
As ever thanks for your time and trouble.

Cheers!
-- Spannerbag
SP Campaigns: After EI (v1.14) Leafsea Burning (v1.18, v1.16)
I suspect the universe is simpler than we think and stranger than we can know.
Also, I fear that beyond a certain point more intelligence does not necessarily benefit a species...
gnombat
Posts: 903
Joined: June 10th, 2010, 8:49 pm

Re: Another basic lua query: optimisation query

Post by gnombat »

Spannerbag 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. :doh:
I'm not sure I understand exactly what you're attempting here, but possibly you want to call 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.
User avatar
Spannerbag
Posts: 787
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Another basic lua query: optimisation query

Post by Spannerbag »

gnombat wrote: Yesterday, 6:20 pm ...call get_units_with_attacks in the evaluation function, and then if there are none, return a score of 0...
D'Oh! - I should've thought of that! :doh: :augh: :doh:
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! :D
wmai_attt.jpg
Cheers!
-- Spannerbag
SP Campaigns: After EI (v1.14) Leafsea Burning (v1.18, v1.16)
I suspect the universe is simpler than we think and stranger than we can know.
Also, I fear that beyond a certain point more intelligence does not necessarily benefit a species...
Post Reply