AI: improve efficiency of move-to-any-enemy CA

The new method avoids a lot of path finding that the previous method did, esp. on large maps with many units and/or enemies, leading to significant speed improvements in some cases.

This fixes #6504
This commit is contained in:
mattsc 2022-07-01 18:24:15 -07:00
parent a6bf253c73
commit b07320f4d0

View file

@ -2,6 +2,10 @@
-- Move AI units toward any enemy on the map. This has a very low CA score and
-- only kicks in when the AI would do nothing else. It prevents the AI from
-- being inactive on maps without enemy leaders and villages.
-- It has a very simple algorithm that does well enough in many cases, but should
-- be considered a fall-back option. If more complex behavior is desired, use
-- the move-to-targets CA and customize it with [goal] tags. That works even
-- on maps without enemy leaders and villages.
local H = wesnoth.require "helper"
local AH = wesnoth.require "ai/lua/ai_helper.lua"
@ -32,30 +36,47 @@ function ca_move_to_any_enemy:evaluation(cfg, data, filter_own)
-- checks this, but we might as well eliminate unreachable enemies right away
local enemies = AH.get_attackable_enemies({}, wesnoth.current.side, { avoid_map = avoid_map })
local unit, destination
-- Find first unit that can reach a hex adjacent to an enemy, and find closest enemy of those reachable.
-- This does not need to find the absolutely best combination, close to that is good enough.
-- Presort unit/enemy pairs by distance in hexes to avoid unnecessary path finding.
-- As AI units are dealt with one by one below, this only needs to be done individually
-- for each AI unit, not for the whole set of pairs.
local unit_distances = {}
for i,u in ipairs(units) do
local best_cost, best_path, best_enemy = AH.no_path
local enemy_distances = {}
for i,e in ipairs(enemies) do
-- We only need to look at adjacent hexes. And we don't worry whether they
-- are occupied by other enemies. If that is the case, no path will be found,
-- but one of those enemies will later be found as potential target.
for xa,ya in H.adjacent_tiles(e.x, e.y) do
if (not avoid_map:get(xa, ya)) then
local path, cost = AH.find_path_with_avoid(u, xa, ya, avoid_map)
local dist = wesnoth.map.distance_between(u, e)
table.insert(enemy_distances, { x = e.x, y = e.y, dist = dist })
end
-- Sort enemies by distance for this AI unit
table.sort(enemy_distances, function(a, b) return (a.dist < b.dist) end)
table.insert(unit_distances, { x = u.x, y = u.y, enemy_distances = enemy_distances })
end
-- Finally, sort AI units by distance to their closest enemies
table.sort(unit_distances, function(a, b) return (a.enemy_distances[1].dist < b.enemy_distances[1].dist) end)
local unit, destination
-- Find the closest enemies to the sorted AI units. This does not need to find the absolutely best
-- combination, such as which hex adjacent to the unit is best, as these enemies are out
-- of reach of the AI (otherwise other CAs would have triggered previously).
-- This moves one AI unit at a time in order to speed up evaluation.
for i,ud in ipairs(unit_distances) do
local u = wesnoth.units.get(ud.x, ud.y)
local best_cost, best_path = AH.no_path
for i,ed in ipairs(ud.enemy_distances) do
-- Only do path finding if the distance to the enemy in less than the current best path cost,
-- otherwise it is impossible to find a shorter path.
if (not best_path) or (ed.dist < best_cost) then
if (not avoid_map:get(ed.x, ed.y)) then
local path, cost = AH.find_path_with_avoid(u, ed.x, ed.y, avoid_map, { ignore_enemies = true })
if (cost < best_cost) then
best_cost = cost
best_path = path
best_enemy = e
-- We also don't care if this is the closest adjacent hex, just pick the first found
break
end
end
end
end
if best_enemy then
if best_path then
MTAE_destination = AH.next_hop(u, nil, nil, { path = best_path, avoid_map = avoid_map })
if (MTAE_destination[1] ~= u.x) or (MTAE_destination[2] ~= u.y) then
MTAE_unit = u