Feasibility of customising return guardian micro ai

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

Moderator: Forum Moderators

gnombat
Posts: 1104
Joined: June 10th, 2010, 8:49 pm

Re: Feasibility of customising return guardian micro ai

Post by gnombat »

Spannerbag wrote: September 3rd, 2026, 4:26 pm Only other minor query: I've not been able to find the internal structure of loc so guessed at how to assign return_loc_uv the relevant x,y values - is there a better way than what I did (line 6 of code below)? Just curious. :)

Code: Select all

local function get_guardian_uv_xy(cfg, guardian_uv)
    local return_loc_uv = AH.get_named_loc_xy('return', cfg)
--    if guardian_uv.variables.return_guardian.return_x and guardian_uv.variables.return_guardian.return_y then
    if guardian_uv.variables.return_guardian then
        if wesnoth.current.map:on_board(guardian_uv.variables.return_guardian.return_x, guardian_uv.variables.return_guardian.return_y) then
            return_loc_uv[1], return_loc_uv[2] = guardian_uv.variables.return_guardian.return_x, guardian_uv.variables.return_guardian.return_y
        end
    end
    return return_loc_uv
end
It looks like the return value of get_named_loc_xy is just an ordinary Lua array with two elements.

See the example in https://www.lua.org/pil/3.6.html:

Code: Select all

days = {"Sunday", "Monday", "Tuesday", "Wednesday",
        "Thursday", "Friday", "Saturday"}
Here days[1] is "Sunday", days[2] is "Monday", etc.

So instead of writing this:

Code: Select all

return_loc_uv[1], return_loc_uv[2] = guardian_uv.variables.return_guardian.return_x, guardian_uv.variables.return_guardian.return_y
You could write this:

Code: Select all

return_loc_uv = { guardian_uv.variables.return_guardian.return_x, guardian_uv.variables.return_guardian.return_y }
I'm not sure if that's any better, though. It might actually be slower (since it creates a new table object instead of reusing an existing one).
User avatar
Spannerbag
Posts: 979
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Feasibility of customising return guardian micro ai

Post by Spannerbag »

gnombat wrote: September 3rd, 2026, 4:49 pm ...So instead of writing this:

Code: Select all

return_loc_uv[1], return_loc_uv[2] = guardian_uv.variables.return_guardian.return_x, guardian_uv.variables.return_guardian.return_y
You could write this:

Code: Select all

return_loc_uv = { guardian_uv.variables.return_guardian.return_x, guardian_uv.variables.return_guardian.return_y }
I'm not sure if that's any better, though. It might actually be slower (since it creates a new table object instead of reusing an existing one).
Thanks for the clarifications, useful to know.
Have moved code from test to dev and will playtest there in due course.
Again, many thanks for your help. :D
Cheers!
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: 979
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Feasibility of customising return guardian micro ai

Post by Spannerbag »

Baffled again. :annoyed:

Plan was to simplify working version of return_guardian_uv to only use unit vars, partly as a lua exercise.
However I'm stuck.

Edit: using v1.19.27 on windows.

Here's the working code:

mai_return_guardian_uv.lua

Code: Select all

--[[	mai_return_guardian_uv.lua

Modified return guardian mai with unit specific return co-ordinates
Many thanks to gnombat for advice and assistance

https://forums.wesnoth.org/viewtopic.php?t=60912
https://wiki.wesnoth.org/LuaAPI/types/unit#Unit_sub-tables
https://github.com/wesnoth/wesnoth/blob/1.18.8/data/ai/lua/ai_helper.lua#L776

If specified, unit variables can override return location specified in [micro_ai] tag.
These are: unit.variables.return_guardian_uv.return_x and unit.variables.return_guardian_uv.return_y

Usage example:

  [micro_ai]
    side=3
    [filter]
    [/filter]
    ai_type=return_guardian_uv
    return_x,return_y=8,1
    action=add
  [/micro_ai]

  # This unit will use return_x,return_y=8,1 above
  {GENERIC_UNIT 3 (Wose Sapling) 8 1}

  # This unit will use return_x,return_y=7,1
  {GENERIC_UNIT 3 (Wose Sapling) 7 1}
  [modify_unit]
    [filter]
      x,y=7,1
    [/filter]
    {VARIABLE return_guardian.return_x 7}
    {VARIABLE return_guardian.return_y 1}
  [/modify_unit]

]]

local AH = wesnoth.require "ai/lua/ai_helper.lua"

function wesnoth.micro_ais.return_guardian_uv(cfg)
	if (cfg.action ~= 'delete') then
		if (not cfg.id) and (not wml.get_child(cfg, "filter")) then
			wml.error("Return Guardian UV [micro_ai] tag requires either id= key or [filter] tag")
		end
		AH.get_named_loc_xy('return', cfg, 'Return guardian UV [micro_ai] tag')
	end
	local required_keys = {}
	local optional_keys = { id = 'string', filter = 'tag',
		return_loc = 'string', return_x = 'integer', return_y = 'integer'	-- REQUIRED but may be overridden by unit variables
	}
	local CA_parms = {
		ai_id = 'mai_return_guardian_uv',
		{ ca_id = 'move', location = '~add-ons/stub19/ai/ca_return_guardian_uv.lua', score = cfg.ca_score or 100100 }
	}
	return required_keys, optional_keys, CA_parms
end
ca_return_guardian_uv.lua

Code: Select all

-- ca_return_guardian_uv.lua

local AH = wesnoth.require "ai/lua/ai_helper.lua"

local function get_guardian_uv(cfg)
    local filter = wml.get_child(cfg, "filter") or { id = cfg.id }
    local guardian_uv = AH.get_units_with_moves {
        side = wesnoth.current.side,
        { "and", filter }
    }[1]
    return guardian_uv
end

local function get_guardian_uv_xy(cfg, guardian_uv)
    local return_loc_uv = AH.get_named_loc_xy('return', cfg)
--    if guardian_uv.variables.return_guardian.return_x and guardian_uv.variables.return_guardian.return_y then
    if guardian_uv.variables.return_guardian then
        if wesnoth.current.map:on_board(guardian_uv.variables.return_guardian.return_x, guardian_uv.variables.return_guardian.return_y) then
            return_loc_uv[1], return_loc_uv[2] = guardian_uv.variables.return_guardian.return_x, guardian_uv.variables.return_guardian.return_y
        end
    end
    return return_loc_uv
end


local ca_return_guardian_uv = {}

function ca_return_guardian_uv:evaluation(cfg)
    local guardian_uv = get_guardian_uv(cfg)
    if guardian_uv then
--      local return_loc = AH.get_named_loc_xy('return', cfg)
        local return_loc = get_guardian_uv_xy(cfg, guardian_uv)
        if (guardian_uv.x == return_loc[1]) and (guardian_uv.y == return_loc[2]) then
            return cfg.ca_score - 200
        else
            return cfg.ca_score
        end
    end

    return 0
end


function ca_return_guardian_uv:execution(cfg)
    local guardian_uv = get_guardian_uv(cfg)
--  local return_loc = AH.get_named_loc_xy('return', cfg)
    local return_loc = get_guardian_uv_xy(cfg, guardian_uv)

    -- In case the return hex is occupied:
    local x, y = return_loc[1], return_loc[2]
    if (guardian_uv.x ~= x) or (guardian_uv.y ~= y) then
        x, y = wesnoth.paths.find_vacant_hex(x, y, guardian_uv)
    end

    local nh = AH.next_hop(guardian_uv, x, y)
    if (not nh) then nh = { guardian_uv.x, guardian_uv.y } end

    AH.movefull_stopunit(ai, guardian_uv, nh)
end

return ca_return_guardian_uv
I simply tried to remove the return_x,return_y elements (code posted further below).
However I've broken something and can't work out what/how.

Nothing in lua console but onscreen and in log get these errors:

error scripting/lua: ~add-ons/stub19/ai/ca_return_guardian_u.lua:12: attempt to index a nil value (local 'guardian_u')
stack traceback:
~add-ons/stub19/ai/ca_return_guardian_u.lua:12: in upvalue 'get_guardian_u'
~add-ons/stub19/ai/ca_return_guardian_u.lua:28: in function <~add-ons/stub19/ai/ca_return_guardian_u.lua:27>
(...tail calls...)

error scripting/lua: ~add-ons/stub19/ai/ca_return_guardian_u.lua:12: attempt to index a nil value (local 'guardian_u')
stack traceback:
~add-ons/stub19/ai/ca_return_guardian_u.lua:12: in upvalue 'get_guardian_u'
~add-ons/stub19/ai/ca_return_guardian_u.lua:28: in function <~add-ons/stub19/ai/ca_return_guardian_u.lua:27>
(...tail calls...)
with_short_wml_error.jpg
It may be coincidence but one sapling doesn't seem to move and the other moves to the specified return location (6,1).


I guess this is complaining that line 12: if guardian_u.variables.return_guardian then references an empty guardian_u?
If I reinstate the original wml.error then the error changes to:

error scripting/lua: ~add-ons/stub19/ai/ca_return_guardian_u.lua:20: ')' expected near 'guardian_u'
stack traceback:
[C]: in field 'dofile'
lua/package.lua:53: in field 'require'
[string "local self, params, data, filter_own = ......"]:2: in main chunk

error scripting/lua: ~add-ons/stub19/ai/ca_return_guardian_u.lua:20: ')' expected near 'guardian_u'
stack traceback:
[C]: in field 'dofile'
lua/package.lua:53: in field 'require'
[string "local self, params, data, filter_own = ......"]:2: in main chunk

error scripting/lua: ~add-ons/stub19/ai/ca_return_guardian_u.lua:20: ')' expected near 'guardian_u'
stack traceback:
[C]: in field 'dofile'
lua/package.lua:53: in field 'require'
[string "local self, params, data, filter_own = ......"]:2: in main chunk

error scripting/lua: ~add-ons/stub19/ai/ca_return_guardian_u.lua:20: ')' expected near 'guardian_u'
stack traceback:
[C]: in field 'dofile'
lua/package.lua:53: in field 'require'
[string "local self, params, data, filter_own = ......"]:2: in main chunk
with_full_wml_error.jpg
Here both saplings seem to behave as if under RCA AI?


Here's the corresponding new versions:

mai_return_guardian_u.lua

Code: Select all

--[[	mai_return_guardian_u.lua

Modified return guardian mai with unit specific return co-ordinates
Many thanks to gnombat for advice and assistance

https://forums.wesnoth.org/viewtopic.php?t=60912
https://wiki.wesnoth.org/LuaAPI/types/unit#Unit_sub-tables
https://github.com/wesnoth/wesnoth/blob/1.18.8/data/ai/lua/ai_helper.lua#L776

Return location specified in unit.variables.return_guardian_u.return_x and unit.variables.return_guardian_u.return_y

Usage example:

  [micro_ai]
    side=3
    [filter]
    [/filter]
    ai_type=return_guardian_u
    action=add
  [/micro_ai]

  # This unit will use return_x,return_y=6,1
  {GENERIC_UNIT 3 (Wose Sapling) 8 1}
  [modify_unit]
    [filter]
      x,y=8,1
    [/filter]
    {VARIABLE return_guardian.return_x 6}
    {VARIABLE return_guardian.return_y 1}
  [/modify_unit]

  # This unit will use it's current position: return_x,return_y=7,1
  {GENERIC_UNIT 3 (Wose Sapling) 7 1}
  [modify_unit]
    [filter]
      x,y=7,1
    [/filter]
    {VARIABLE return_guardian.return_x 7}
    {VARIABLE return_guardian.return_y 1}
  [/modify_unit]

]]

local AH = wesnoth.require "ai/lua/ai_helper.lua"

function wesnoth.micro_ais.return_guardian_u(cfg)
	if (cfg.action ~= 'delete') then
		if (not cfg.id) and (not wml.get_child(cfg, "filter")) then
			wml.error("Return Guardian U [micro_ai] tag requires either id= key or [filter] tag")
		end
--		AH.get_named_loc_xy('return', cfg, 'Return guardian UV [micro_ai] tag')			-- REMOVED
	end
	local required_keys = {}
	local optional_keys = { id = 'string', filter = 'tag'}						-- REMOVED "," added "}"
--		return_loc = 'string', return_x = 'integer', return_y = 'integer'			-- REMOVED
--	}												-- REMOVED
	local CA_parms = {
		ai_id = 'mai_return_guardian_u',
		{ ca_id = 'move', location = '~add-ons/stub19/ai/ca_return_guardian_u.lua', score = cfg.ca_score or 100100 }
	}
	return required_keys, optional_keys, CA_parms
end
ca_return_guardian_u.lua

Code: Select all

-- ca_return_guardian_u.lua

local AH = wesnoth.require "ai/lua/ai_helper.lua"

local function get_guardian_u(cfg)
    local filter = wml.get_child(cfg, "filter") or { id = cfg.id }
    local guardian_u = AH.get_units_with_moves {
        side = wesnoth.current.side,
        { "and", filter }
    }[1]
    local onmap = false
    if guardian_u.variables.return_guardian then
        if wesnoth.current.map:on_board(guardian_u.variables.return_guardian.return_x, guardian_u.variables.return_guardian.return_y) then
            onmap = true
        end
    end
    if onmap then
        return guardian_u
    else
--        wml.error("Micro AI get_guardian_u unit id= " .. guardian_u.id .. " currently at x,y= " .. guardian_u.x .. "," guardian_u.y .. " has invalid return_x,return_y= " .. guardian_u.variables.return_guardian.return_x .. "," .. guardian_u.variables.return_guardian.return_y)
        wml.error("Micro AI get_guardian_u error msg")
    end
end

local ca_return_guardian_u = {}

function ca_return_guardian_u:evaluation(cfg)
    local guardian_u = get_guardian_u(cfg)
    if guardian_u then
        if (guardian_u.x == guardian_u.variables.return_guardian.return_x) and (guardian_u.y == guardian_u.variables.return_guardian.return_y) then
            return cfg.ca_score - 200
        else
            return cfg.ca_score
        end
    end

    return 0
end


function ca_return_guardian_u:execution(cfg)
    local guardian_u = get_guardian_u(cfg)

    -- In case the return hex is occupied:
    local x, y = guardian_u.variables.return_guardian.return_x, guardian_u.variables.return_guardian.return_y
    if (guardian_u.x ~= x) or (guardian_u.y ~= y) then
        x, y = wesnoth.paths.find_vacant_hex(x, y, guardian_u)
    end

    local nh = AH.next_hop(guardian_u, x, y)
    if (not nh) then nh = { guardian_u.x, guardian_u.y } end

    AH.movefull_stopunit(ai, guardian_u, nh)
end

return ca_return_guardian_u

WML

Code: Select all

[event]
  name=side 1 turn 1

  [micro_ai]
    side=3
    [filter]
    [/filter]
    ai_type=return_guardian_u
    action=add
  [/micro_ai]

  # This unit will use return_x,return_y=6,1
  {GENERIC_UNIT 3 (Wose Sapling) 8 1}
  [modify_unit]
    [filter]
      x,y=8,1
    [/filter]
    {VARIABLE return_guardian.return_x 6}
    {VARIABLE return_guardian.return_y 1}
  [/modify_unit]

  # This unit will use it's current position: return_x,return_y=7,1
  {GENERIC_UNIT 3 (Wose Sapling) 7 1}
  [modify_unit]
    [filter]
      x,y=7,1
    [/filter]
    {VARIABLE return_guardian.return_x 7}
    {VARIABLE return_guardian.return_y 1}
  [/modify_unit]
...
As I say, despite all this verbiage I'm sure it's something stupid, probably a parameter mis-match caused by removing
AH.get_named_loc_xy('return', cfg, 'Return guardian UV [micro_ai] tag')
and/or hacking

Code: Select all

	local optional_keys = { id = 'string', filter = 'tag',
		return_loc = 'string', return_x = 'integer', return_y = 'integer'	-- REQUIRED but may be overridden by unit variables
to
local optional_keys = { id = 'string', filter = 'tag'}

However I've spent a lot of time on this and I think a fresh pair of eyes would help.

Cheers!
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: 1104
Joined: June 10th, 2010, 8:49 pm

Re: Feasibility of customising return guardian micro ai

Post by gnombat »

Spannerbag wrote: September 8th, 2026, 4:07 pm I guess this is complaining that line 12: if guardian_u.variables.return_guardian then references an empty guardian_u?
Yes, AH.get_units_with_moves() might not return any units. In that case guardian_u will be nil. You need to check for this before trying to do anything with guardian_u.
User avatar
Spannerbag
Posts: 979
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Feasibility of customising return guardian micro ai

Post by Spannerbag »

gnombat wrote: September 8th, 2026, 9:38 pm
Spannerbag wrote: September 8th, 2026, 4:07 pm I guess this is complaining that line 12: if guardian_u.variables.return_guardian then references an empty guardian_u?
Yes, AH.get_units_with_moves() might not return any units. In that case guardian_u will be nil. You need to check for this before trying to do anything with guardian_u.
Hmm, pretty sure it should find both the test units as they would both have full moves (created side 1 turn 1 but in side 3 so would have a turn refresh before being controlled by micro ai).
I think I'll have to sprinkle some comments in the code and see what's going on.
Oh well, that's a job for another day, can't face more lua today! :? :augh:

Thanks for your help.

Cheers!
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: 1104
Joined: June 10th, 2010, 8:49 pm

Re: Feasibility of customising return guardian micro ai

Post by gnombat »

Spannerbag wrote: September 8th, 2026, 11:03 pm Hmm, pretty sure it should find both the test units as they would both have full moves (created side 1 turn 1 but in side 3 so would have a turn refresh before being controlled by micro ai).
What about if they don't have any moves remaining?
User avatar
Spannerbag
Posts: 979
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Feasibility of customising return guardian micro ai

Post by Spannerbag »

gnombat wrote: September 8th, 2026, 11:10 pm What about if they don't have any moves remaining?
Is it possible for the same unit to be processed multiple times by a micro ai?
If so then yes I suppose it could happen.
Bit doubtful tho' TBH because this issue only arises when I use the version that doesn't have return_x, return_y keys inside [micro_ai]; the versions that do work fine.

Will have to systematically test all this however some time in the next few days when I get chance.

Thanks again for your help!

Cheers!
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: 1104
Joined: June 10th, 2010, 8:49 pm

Re: Feasibility of customising return guardian micro ai

Post by gnombat »

Spannerbag wrote: September 8th, 2026, 11:25 pm Is it possible for the same unit to be processed multiple times by a micro ai?
If so then yes I suppose it could happen.
In general, the evaluation function will keep getting called over and over again until it returns a score of zero.
Spannerbag wrote: September 8th, 2026, 11:25 pm Bit doubtful tho' TBH because this issue only arises when I use the version that doesn't have return_x, return_y keys inside [micro_ai]; the versions that do work fine.
For the original ca_return_guardian, the evaluation function returns zero when get_guardian() returns a nil value, which happens when get_units_with_moves() returns no units. So this is always going to happen eventually.
User avatar
Spannerbag
Posts: 979
Joined: December 18th, 2016, 6:14 pm
Location: Yes

Re: Feasibility of customising return guardian micro ai

Post by Spannerbag »

gnombat wrote: September 9th, 2026, 12:39 am ...So this is always going to happen eventually.
Thanks for that last info-nugget, it helped. :D

Based on somewhat sparse testing I think I have two working return_guardian variants. :shock:
  1. return_guardian_u, only uses unit variables for return co-ordinates.
  2. return_guardian_ur as above but falls back to return_x,treturn_y specified in [micro_ai] if unit variables absent or invalid.
The return logic seems to work (although the RCA combat probably needs some tweaking for my purposes) and the basic error trapping works.
Happy to share this code if anyone's interested (I'm also fairly sure some of my code is rather klunky so would also appreciate any improvements).

Thanks again for your help, really much appreciated.

Cheers!
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