Messenger MAI: allow several messengers to be controlled by AI

For two of the CAs this mostly just means reading the [filter] tag and
picking one of the messengers.  However, for the CA moving the escort
units, it means a complete rewrite, as the AI now needs to figure out
which escort unit should be moved toward which messenger.
This commit is contained in:
mattsc 2014-03-27 13:36:29 -07:00
parent 2d458b642a
commit 62f709aaa3
5 changed files with 115 additions and 65 deletions

View file

@ -68,10 +68,11 @@ local function messenger_find_clearing_attack(unit, goal_x, goal_y, cfg)
--print('Finding attacks on',enemy_in_way.name,enemy_in_way.id)
-- Find all units that can attack this enemy
local filter = cfg.filter or { id = cfg.id }
local my_units = wesnoth.get_units {
side = wesnoth.current.side,
formula = '$this_unit.attacks_left > 0',
{ "not", { id = unit.id } },
{ "not", filter },
{ "and", cfg.filter_second }
}
@ -134,15 +135,12 @@ local function messenger_find_clearing_attack(unit, goal_x, goal_y, cfg)
end
function ca_messenger_attack:evaluation(ai, cfg, self)
-- Attack units in the path of the messenger
-- id: id of the messenger unit
-- Attack units in the path of the messengers
-- goal_x, goal_y: coordinates of the goal toward which the messenger moves
local messenger = wesnoth.get_units{ side = wesnoth.current.side, id = cfg.id }[1]
local messenger, x, y = messenger_next_waypoint(cfg)
if (not messenger) then return 0 end
local x, y = messenger_next_waypoint(messenger, cfg, self)
-- See if there's an enemy in the way that should be attacked
local attack = messenger_find_clearing_attack(messenger, x, y, cfg)

View file

@ -1,69 +1,92 @@
local H = wesnoth.require "lua/helper.lua"
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local LS = wesnoth.require "lua/location_set.lua"
local ca_messenger_escort_move = {}
local messenger_next_waypoint = wesnoth.require "ai/micro_ais/cas/ca_messenger_f_next_waypoint.lua"
function ca_messenger_escort_move:evaluation(ai, cfg)
-- Move escort units close to messenger, and in between messenger and enemies
-- The messenger has moved at this time, so we don't need to exclude him
-- But we check that he exist (not for this scenario, but for others)
local messenger = wesnoth.get_units{ side = wesnoth.current.side, id = cfg.id }[1]
if (not messenger) then return 0 end
-- Move escort units close to messengers, and in between messengers and enemies
-- The messengers have moved at this time, so we don't need to exclude them,
-- but we check that there are messengers left
local my_units = wesnoth.get_units {
side = wesnoth.current.side,
formula = '$this_unit.moves > 0',
{ "and", cfg.filter_second }
}
if (not my_units[1]) then return 0 end
if my_units[1] then
return cfg.ca_score
end
return 0
local _, _, _, messengers = messenger_next_waypoint(cfg)
if (not messengers) or (not messengers[1]) then return 0 end
return cfg.ca_score
end
function ca_messenger_escort_move:execution(ai, cfg)
local messenger = wesnoth.get_units{ id = cfg.id }[1]
local my_units = wesnoth.get_units {
side = wesnoth.current.side,
formula = '$this_unit.moves > 0',
{ "and", cfg.filter_second }
}
-- Simply move units one at a time
local next_unit = my_units[1]
local reach = LS.of_pairs(wesnoth.find_reach(next_unit))
-- Distance from messenger for each hex the unit can reach
local dist_messenger = AH.distance_map({messenger}, reach)
local _, _, _, messengers = messenger_next_waypoint(cfg)
local enemies = wesnoth.get_units {
{ "filter_side", {{"enemy_of", {side = wesnoth.current.side} }} }
}
-- Rating (in the end, we pick the _minimum _rating):
-- 1. Minimize distance from enemies
local rating = AH.distance_map(enemies, reach)
-- 2. This one favors hexes in between messenger and enemies
rating:union_merge(dist_messenger, function(x, y, v1, v2)
return v1 + v2*#enemies
end)
-- 3. Strongly prefer hexes close to the messenger
rating:union_merge(dist_messenger, function(x, y, v1, v2)
return v1 + v2^2
end)
--AH.put_labels(rating)
local base_rating_map = LS.create()
local max_rating, best_unit, best_hex = -9e99
for _,unit in ipairs(my_units) do
-- Only considering hexes unoccupied by other units is good enough for this
local reach_map = AH.get_reachable_unocc(unit)
-- Now find hex with minimum value that is unoccupied
min_rating, best_hex = 9e99, {}
rating:iter(function(x, y, r)
local unit_in_way = wesnoth.get_units{ x = x, y = y, { "not", { id = next_unit.id } } }[1]
if (not unit_in_way) and (r < min_rating) then
min_rating, best_hex = r, { x, y }
end
end)
-- and move the unit there
AH.movefull_stopunit(ai, next_unit, best_hex)
-- Minor rating for the fastest and strongest unit to go first
local unit_rating = unit.max_moves / 100. + unit.hitpoints / 1000.
reach_map:iter( function(x, y, v)
-- base rating only needs to be calculated once for each hex
local base_rating = base_rating_map:get(x, y)
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 most through the waypoints
local max_messenger_rating = -9e99
for _,m in ipairs(messengers) do
local messenger_rating = 1. / (H.distance_between(x, y, m.x, m.y) + 2.)
messenger_rating = messenger_rating * 10. * (1. + m.variables.wp_rating * 2.)
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. / (H.distance_between(x, y, e.x, e.y) + 2.)
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
--AH.put_labels(base_rating_map)
-- This will always find at least the hex the unit is on -> no check necessary
AH.movefull_stopunit(ai, best_unit, best_hex)
end
return ca_messenger_escort_move

View file

@ -1,9 +1,17 @@
local H = wesnoth.require "lua/helper.lua"
local AH = wesnoth.require "ai/lua/ai_helper.lua"
return function(messenger, cfg, self)
-- Variable to store which waypoint to go to next (persistent)
if (not self.data.next_waypoint) then self.data.next_waypoint = 1 end
return function(cfg)
-- Calculate next waypoint and rating for all messengers
-- Return next messenger to move and waypoint toward which it should move
-- Also return the array of all messengers (for escort move evaluation,
-- so that it only needs to be done in one place, in case the syntax is changed some more)
-- Returns nil for first 3 arguments if no messenger has moves left
-- Returns nil for all arguments if there are no messengers on the map
local filter = cfg.filter or { id = cfg.id }
local messengers = wesnoth.get_units { side = wesnoth.current.side, { "and", filter } }
if (not messengers[1]) then return end
local waypoint_x = AH.split(cfg.waypoint_x, ",")
local waypoint_y = AH.split(cfg.waypoint_y, ",")
@ -12,14 +20,34 @@ return function(messenger, cfg, self)
waypoint_y[i] = tonumber(waypoint_y[i])
end
-- If we're within 3 hexes of the next waypoint, we go on to the one after that
-- except if that one's the last one already
local dist_wp = H.distance_between(messenger.x, messenger.y,
waypoint_x[self.data.next_waypoint], waypoint_y[self.data.next_waypoint]
)
if (dist_wp <= 3) and (self.data.next_waypoint < #waypoint_x) then
self.data.next_waypoint = self.data.next_waypoint + 1
-- Set the next waypoint for all messengers
-- Also find those with MP left and return the one to next move, together with the WP to move toward
local max_rating, best_messenger, x, y = -9e99
for i, m in ipairs(messengers) do
-- To avoid code duplication and ensure consistency, we store some pieces of
-- information in the messenger units, even though it could be calculated each time it is needed
local wp_i = m.variables.wp_i or 1
local wp_x, wp_y = waypoint_x[wp_i], waypoint_y[wp_i]
-- If this messenger is within 3 hexes of the next waypoint, we go on to the one after that
-- except if that one's the last one already
local dist_wp = H.distance_between(m.x, m.y, wp_x, wp_y)
if (dist_wp <= 3) and (wp_i < #waypoint_x) then wp_i = wp_i + 1 end
m.variables.wp_i, m.variables.wp_x, m.variables.wp_y = wp_i, wp_x, wp_y
-- Also store the rating for each messenger
-- For now, this is simply a "forward rating"
local rating = wp_i - dist_wp / 1000.
m.variables.wp_rating = rating
-- Find the messenger with the highest rating that has MP left
if (m.moves > 0) and (rating > max_rating) then
best_messenger, max_rating = m, rating
x, y = wp_x, wp_y
end
end
return waypoint_x[self.data.next_waypoint], waypoint_y[self.data.next_waypoint]
return best_messenger, x, y, messengers
end

View file

@ -6,10 +6,10 @@ local ca_messenger_move = {}
local messenger_next_waypoint = wesnoth.require "ai/micro_ais/cas/ca_messenger_f_next_waypoint.lua"
function ca_messenger_move:evaluation(ai, cfg)
-- Move the messenger (unit with passed id) toward goal, attack adjacent unit if possible
-- Move the messenger toward goal, attack adjacent unit if possible
-- without retaliation or little expected damage with high chance of killing the enemy
local messenger = wesnoth.get_units{ id = cfg.id, formula = '$this_unit.moves > 0' }[1]
local messenger = messenger_next_waypoint(cfg)
if messenger then
return cfg.ca_score
@ -17,10 +17,8 @@ function ca_messenger_move:evaluation(ai, cfg)
return 0
end
function ca_messenger_move:execution(ai, cfg, self)
local messenger = wesnoth.get_units{ id = cfg.id, formula = '$this_unit.moves > 0' }[1]
local x, y = messenger_next_waypoint(messenger, cfg, self)
function ca_messenger_move:execution(ai, cfg)
local messenger, x, y = messenger_next_waypoint(cfg)
if (messenger.x ~= x) or (messenger.y ~= y) then
local wp = AH.get_closest_location(
@ -100,7 +98,7 @@ function ca_messenger_move:execution(ai, cfg, self)
local targets = wesnoth.get_units {
{ "filter_side", { {"enemy_of", {side = wesnoth.current.side} } } },
{ "filter_adjacent", { id = cfg.id } }
{ "filter_adjacent", { id = messenger.id } }
}
local max_rating, best_tar, best_weapon = -9e99, {}, -1
@ -141,7 +139,7 @@ function ca_messenger_move:execution(ai, cfg, self)
x = tonumber(waypoint_x[#waypoint_x]),
y = tonumber(waypoint_y[#waypoint_y]),
{ "filter_side", { {"enemy_of", {side = wesnoth.current.side} } } },
{ "filter_adjacent", { id = cfg.id } }
{ "filter_adjacent", { id = messenger.id } }
}[1]
if target then

View file

@ -86,8 +86,11 @@ function wesnoth.wml_actions.micro_ai(cfg)
--------- Messenger Escort Micro AI ------------------------------------
elseif (cfg.ai_type == 'messenger_escort') then
required_keys = { "id", "waypoint_x", "waypoint_y" }
optional_keys = { "enemy_death_chance", "filter_second", "messenger_death_chance" }
if (not cfg.id) and (not H.get_child(cfg, "filter")) then
H.wml_error("Messenger [micro_ai] tag requires either id= key or [filter] tag")
end
required_keys = { "waypoint_x", "waypoint_y" }
optional_keys = { "id", "enemy_death_chance", "filter", "filter_second", "messenger_death_chance" }
local score = cfg.ca_score or 300000
CA_parms = {
{ ca_id = 'mai_messenger_attack', location = CA_path .. 'ca_messenger_attack.lua', score = score },