Merge pull request #5207 from mattsc/messenger_mai_avoid

Messenger Micro AI: add [avoid] tag functionality
This commit is contained in:
mattsc 2021-03-13 19:09:09 -08:00 committed by GitHub
commit d45f1d3e3d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 90 additions and 57 deletions

View file

@ -295,6 +295,9 @@ function ai_helper.robust_move_and_attack(ai, src, dst, target_loc, cfg)
-- parameter is true, a partial move is done instead.
-- weapon: The number (starting at 1) of the attack weapon to be used.
-- If omitted, the best weapon is automatically selected.
-- avoid_map (location set): if given, the hexes in avoid_map are excluded
-- This is only used for passing through to move_unit_out_of_way. It is assumed
-- that @dst has been checked to lie outside areas to avoid.
-- all optional parameters for ai_helper.move_unit_out_of_way()
-- Notes:
@ -739,11 +742,12 @@ function ai_helper.get_locations_no_borders(location_filter)
return locs
end
function ai_helper.get_closest_location(hex, location_filter, unit)
function ai_helper.get_closest_location(hex, location_filter, unit, avoid_map)
-- Get the location closest to @hex (in format { x, y })
-- that matches @location_filter (in WML table format)
-- @unit can be passed as an optional third parameter, in which case the
-- terrain needs to be passable for that unit
-- @avoid_map (location set): if given, the hexes in avoid_map are excluded
-- Returns nil if no terrain matching the filter was found
-- Find the maximum distance from 'hex' that's possible on the map
@ -780,6 +784,14 @@ function ai_helper.get_closest_location(hex, location_filter, unit)
local locs = wesnoth.map.find(loc_filter)
if avoid_map then
for i = #locs,1,-1 do
if avoid_map:get(locs[i][1], locs[i][2]) then
table.remove(locs, i)
end
end
end
if unit then
for _,loc in ipairs(locs) do
local movecost = unit:movement_on(wesnoth.current.map[loc])
@ -1958,6 +1970,7 @@ function ai_helper.move_unit_out_of_way(ai, unit, cfg)
-- Main rating is the moves the unit still has left after that
-- Other, configurable, parameters are given to function in @cfg:
-- dx, dy: the direction in which moving out of the way is preferred
-- avoid_map (location set): if given, the hexes in avoid_map are excluded
-- labels: if set, display labels of the rating for each hex the unit can reach
-- viewing_side: see comments at beginning of this file. Defaults to side of @unit.
-- ignore_visibility: see comments at beginning of this file. Defaults to nil.
@ -1983,17 +1996,20 @@ function ai_helper.move_unit_out_of_way(ai, unit, cfg)
if (not unit_in_way) -- also excludes current hex
or ((not ignore_visibility) and (not ai_helper.is_visible_unit(viewing_side, unit_in_way)))
then
local rating = loc[3] -- also disfavors hexes next to visible enemy units for which loc[3] = 0
local avoid_this_hex = cfg and cfg.avoid_map and cfg.avoid_map:get(loc[1], loc[2])
if (not avoid_this_hex) then
local rating = loc[3] -- also disfavors hexes next to visible enemy units for which loc[3] = 0
if dx then
rating = rating + (loc[1] - unit.x) * dx * 0.01
rating = rating + (loc[2] - unit.y) * dy * 0.01
end
if dx then
rating = rating + (loc[1] - unit.x) * dx * 0.01
rating = rating + (loc[2] - unit.y) * dy * 0.01
end
if cfg.labels then reach_map:insert(loc[1], loc[2], rating) end
if cfg.labels then reach_map:insert(loc[1], loc[2], rating) end
if (rating > max_rating) then
max_rating, best_hex = rating, { loc[1], loc[2] }
if (rating > max_rating) then
max_rating, best_hex = rating, { loc[1], loc[2] }
end
end
end
end

View file

@ -4,13 +4,13 @@ local AH = wesnoth.require "ai/lua/ai_helper.lua"
local messenger_next_waypoint = wesnoth.require "ai/micro_ais/cas/ca_messenger_f_next_waypoint.lua"
local best_attack
local function messenger_find_enemies_in_way(messenger, goal_x, goal_y)
local function messenger_find_enemies_in_way(messenger, goal_x, goal_y, avoid_map)
-- Returns the first unit on or next to the path of the messenger
-- @messenger: proxy table for the messenger unit
-- @goal_x,@goal_y: coordinates of the goal toward which the messenger moves
-- Returns proxy table for the first unit found, or nil if none was found
local path, cost = AH.find_path_with_shroud(messenger, goal_x, goal_y, { ignore_units = true })
local path, cost = AH.find_path_with_avoid(messenger, goal_x, goal_y, avoid_map, { ignore_enemies = true })
if cost >= 42424242 then return end
-- The second path hex is the first that is important for the following analysis
@ -23,7 +23,7 @@ local function messenger_find_enemies_in_way(messenger, goal_x, goal_y)
-- After that, go through adjacent hexes of all the other path hexes
for i = 2,#path do
local sub_path, sub_cost = AH.find_path_with_shroud(messenger, path[i][1], path[i][2], { ignore_units = true })
local sub_path, sub_cost = AH.find_path_with_avoid(messenger, path[i][1], path[i][2], avoid_map, { ignore_enemies = true })
if (sub_cost <= messenger.moves) then
for xa,ya in H.adjacent_tiles(path[i][1], path[i][2]) do
local enemy = wesnoth.units.get(xa, ya)
@ -42,7 +42,9 @@ local function messenger_find_clearing_attack(messenger, goal_x, goal_y, cfg)
-- @goal_x,@goal_y: coordinates of the goal toward which the messenger moves
-- Returns proxy table containing the attack, or nil if none was found
local enemy_in_way = messenger_find_enemies_in_way(messenger, goal_x, goal_y)
local avoid_map = AH.get_avoid_map(ai, wml.get_child(cfg, "avoid"), true)
local enemy_in_way = messenger_find_enemies_in_way(messenger, goal_x, goal_y, avoid_map)
if (not enemy_in_way) then return end
local filter = wml.get_child(cfg, "filter") or { id = cfg.id }
@ -57,8 +59,9 @@ local function messenger_find_clearing_attack(messenger, goal_x, goal_y, cfg)
local max_rating = - math.huge
for _,attack in ipairs(attacks) do
if (attack.target.x == enemy_in_way.x) and (attack.target.y == enemy_in_way.y) then
if (attack.target.x == enemy_in_way.x) and (attack.target.y == enemy_in_way.y)
and (not avoid_map:get(attack.dst.x, attack.dst.y))
then
-- Rating: expected HP of attacker and defender
local rating = attack.att_stats.average_hp - 2 * attack.def_stats.average_hp
@ -74,16 +77,18 @@ local function messenger_find_clearing_attack(messenger, goal_x, goal_y, cfg)
--> try to fight our way to that enemy
for _,attack in ipairs(attacks) do
-- Rating: expected HP of attacker and defender
local rating = attack.att_stats.average_hp - 2 * attack.def_stats.average_hp
if (not avoid_map:get(attack.dst.x, attack.dst.y)) then
local rating = attack.att_stats.average_hp - 2 * attack.def_stats.average_hp
-- Give a huge bonus for closeness to enemy_in_way
local tmp_defender = wesnoth.units.get(attack.target.x, attack.target.y)
local dist = wesnoth.map.distance_between(enemy_in_way.x, enemy_in_way.y, tmp_defender.x, tmp_defender.y)
-- Give a huge bonus for closeness to enemy_in_way
local tmp_defender = wesnoth.units.get(attack.target.x, attack.target.y)
local dist = wesnoth.map.distance_between(enemy_in_way.x, enemy_in_way.y, tmp_defender.x, tmp_defender.y)
rating = rating + 100. / dist
rating = rating + 100. / dist
if (rating > max_rating) then
max_rating, best_attack = rating, attack
if (rating > max_rating) then
max_rating, best_attack = rating, attack
end
end
end
@ -108,7 +113,8 @@ function ca_messenger_attack:evaluation(cfg)
end
function ca_messenger_attack:execution(cfg)
AH.robust_move_and_attack(ai, best_attack.src, best_attack.dst, best_attack.target)
local avoid_map = AH.get_avoid_map(ai, wml.get_child(cfg, "avoid"), true)
AH.robust_move_and_attack(ai, best_attack.src, best_attack.dst, best_attack.target, { avoid_map = avoid_map })
best_attack = nil
end

View file

@ -32,6 +32,8 @@ function ca_messenger_escort_move:execution(cfg)
local escorts = get_escorts(cfg)
local _, _, _, messengers = messenger_next_waypoint(cfg)
local avoid_map = AH.get_avoid_map(ai, wml.get_child(cfg, "avoid"), true)
local enemies = AH.get_attackable_enemies()
local base_rating_map = LS.create()
@ -44,40 +46,42 @@ function ca_messenger_escort_move:execution(cfg)
local unit_rating = unit.max_moves / 100. + unit.hitpoints / 1000.
reach_map:iter( function(x, y, v)
local base_rating = base_rating_map:get(x, y)
if (not avoid_map:get(x, y)) or ((x == unit.x) and (y == unit.y)) then
local base_rating = base_rating_map:get(x, y)
if (not base_rating) then
base_rating = 0
if (not base_rating) then
base_rating = 0
-- Distance from messenger is most important; only closest messenger counts for this
-- Give somewhat of a bonus for the messenger that has moved the farthest through the waypoints
local max_messenger_rating = - math.huge
for _,m in ipairs(messengers) do
local messenger_rating = 1. / (M.distance_between(x, y, m.x, m.y) + 2.)
local wp_rating = MAIUV.get_mai_unit_variables(m, cfg.ai_id, "wp_rating")
messenger_rating = messenger_rating * 10. * (1. + wp_rating * 2.)
-- Distance from messenger is most important; only closest messenger counts for this
-- Give somewhat of a bonus for the messenger that has moved the farthest through the waypoints
local max_messenger_rating = - math.huge
for _,m in ipairs(messengers) do
local messenger_rating = 1. / (M.distance_between(x, y, m.x, m.y) + 2.)
local wp_rating = MAIUV.get_mai_unit_variables(m, cfg.ai_id, "wp_rating")
messenger_rating = messenger_rating * 10. * (1. + wp_rating * 2.)
if (messenger_rating > max_messenger_rating) then
max_messenger_rating = messenger_rating
if (messenger_rating > max_messenger_rating) then
max_messenger_rating = messenger_rating
end
end
base_rating = base_rating + max_messenger_rating
-- Distance from (sum of) enemies is important too
-- This favors placing escort units between the messenger and close enemies
for _,e in ipairs(enemies) do
base_rating = base_rating + 1. / (M.distance_between(x, y, e.x, e.y) + 2.)
end
base_rating_map:insert(x, y, base_rating)
end
base_rating = base_rating + max_messenger_rating
local rating = base_rating + unit_rating
-- Distance from (sum of) enemies is important too
-- This favors placing escort units between the messenger and close enemies
for _,e in ipairs(enemies) do
base_rating = base_rating + 1. / (M.distance_between(x, y, e.x, e.y) + 2.)
if (rating > max_rating) then
max_rating = rating
best_unit, best_hex = unit, { x, y }
end
base_rating_map:insert(x, y, base_rating)
end
local rating = base_rating + unit_rating
if (rating > max_rating) then
max_rating = rating
best_unit, best_hex = unit, { x, y }
end
end)
end

View file

@ -16,23 +16,30 @@ end
function ca_messenger_move:execution(cfg)
local messenger, x, y = messenger_next_waypoint(cfg)
local avoid_map = AH.get_avoid_map(ai, wml.get_child(cfg, "avoid"), true)
if (messenger.x ~= x) or (messenger.y ~= y) then
local wp = AH.get_closest_location(
{ x, y },
{ { "not", { { "filter", { { "not", { side = wesnoth.current.side } } } } } } },
messenger
messenger, avoid_map
)
x, y = wp[1], wp[2]
end
local next_hop = AH.next_hop(messenger, x, y, { ignore_own_units = true } )
local avoid_map = AH.get_avoid_map(ai, wml.get_child(cfg, "avoid"), true)
local path = AH.find_path_with_avoid(messenger, x, y, avoid_map)
if (not path) then path = { { messenger.x, messenger.y } } end
local next_hop = AH.next_hop(messenger, x, y, { path = path, avoid_map = avoid_map } )
if (not next_hop) then next_hop = { messenger.x, messenger.y } end
-- Compare this to the "ideal path"
local path = AH.find_path_with_shroud(messenger, x, y, { ignore_units = 'yes' })
local path = AH.find_path_with_avoid(messenger, x, y, avoid_map, { ignore_enemies = true })
if (not path) then path = { { messenger.x, messenger.y } } end
local optimum_hop = { messenger.x, messenger.y }
for _,step in ipairs(path) do
local sub_path, sub_cost = AH.find_path_with_shroud(messenger, step[1], step[2])
local sub_path, sub_cost = AH.find_path_with_avoid(messenger, step[1], step[2], avoid_map)
if sub_cost > messenger.moves then
break
else
@ -60,14 +67,14 @@ function ca_messenger_move:execution(cfg)
if unit_in_way then unit_in_way:extract() end
messenger.loc = { next_hop[1], next_hop[2] }
local _, cost1 = AH.find_path_with_shroud(messenger, x, y, { ignore_units = 'yes' })
local _, cost1 = AH.find_path_with_avoid(messenger, x, y, avoid_map, { ignore_enemies = true })
local unit_in_way2 = wesnoth.units.get(optimum_hop[1], optimum_hop[2])
if (unit_in_way2 == messenger) then unit_in_way2 = nil end
if unit_in_way2 then unit_in_way2:extract() end
messenger.loc = { optimum_hop[1], optimum_hop[2] }
local _, cost2 = AH.find_path_with_shroud(messenger, x, y, { ignore_units = 'yes' })
local _, cost2 = AH.find_path_with_avoid(messenger, x, y, avoid_map, { ignore_enemies = true })
messenger.loc = { x_current, y_current }
if unit_in_way then unit_in_way:to_map() end
@ -78,7 +85,7 @@ function ca_messenger_move:execution(cfg)
if (cost2 + messenger.max_moves/2 < cost1) then next_hop = optimum_hop end
if next_hop and ((next_hop[1] ~= messenger.x) or (next_hop[2] ~= messenger.y)) then
AH.robust_move_and_attack(ai, messenger, next_hop)
AH.robust_move_and_attack(ai, messenger, next_hop, nil, { avoid_map = avoid_map })
else
AH.checked_stopunit_moves(ai, messenger)
end

View file

@ -8,7 +8,7 @@ function wesnoth.micro_ais.messenger_escort(cfg)
AH.get_multi_named_locs_xy('waypoint', cfg, 'Messenger [micro_ai] tag')
end
local required_keys = {}
local optional_keys = { "id", "enemy_death_chance", "[filter]", "[filter_second]", "invert_order", "messenger_death_chance", "waypoint_loc", "waypoint_x", "waypoint_y" }
local optional_keys = { "[avoid]", "id", "enemy_death_chance", "[filter]", "[filter_second]", "invert_order", "messenger_death_chance", "waypoint_loc", "waypoint_x", "waypoint_y" }
local score = cfg.ca_score or 300000
local CA_parms = {
ai_id = 'mai_messenger',