Fast Micro AI: separate out attacks by AI leader

In the previous version, the leader would sometimes leave the keep for
truly suicidal attacks. This commit adds an extra layer of analysis to
evaluate whether an attack position is safe enough.

This evaluation is too expensive to be done for all units for the
purpose of this AI, or even to be done for the leader more than once,
so this is separated out into its own candidate action that is only
evaluated once everything else is done.
This commit is contained in:
mattsc 2015-09-07 08:48:57 -07:00
parent cf18825a82
commit 5533d5728a
3 changed files with 195 additions and 4 deletions

View file

@ -27,8 +27,10 @@ function ca_fast_combat:evaluation(ai, cfg, self)
if (not self.data.fast_combat_units) or (not self.data.fast_combat_units[1]) then
self.data.fast_combat_units = wesnoth.get_units {
side = wesnoth.current.side,
canrecruit = 'no',
{ "and", filter_own }
}
if (not self.data.fast_combat_units[1]) then return 0 end
-- For speed reasons, we'll go through the arrays from the end, so they are sorted backwards

View file

@ -0,0 +1,189 @@
local H = wesnoth.require "lua/helper.lua"
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local FAU = wesnoth.require "ai/micro_ais/cas/ca_fast_attack_utils.lua"
local LS = wesnoth.require "lua/location_set.lua"
local ca_fast_combat_leader = {}
function ca_fast_combat_leader:evaluation(ai, cfg, self)
-- Some parts of this are very similar to ca_fast_combat.lua.
-- However, for speed reasons we really want this to be a separate CA, so
-- that the (more expensive) calculations for keeping the leader safe only
-- get done once, when all other Fast MAI CAs are entirely done.
leader_weight = (cfg and cfg.leader_weight) or 2
leader_attack_max_units = (cfg and cfg.leader_attack_max_units) or 3
self.data.move_cache = { turn = wesnoth.current.turn }
self.data.gamedata = FAU.gamedata_setup()
local filter_own = cfg.filter
local filter_enemy = cfg.filter_second
if (not filter_own) and (not filter_enemy) then
local ai_tag = H.get_child(wesnoth.sides[wesnoth.current.side].__cfg, 'ai')
for aspect in H.child_range(ai_tag, 'aspect') do
if (aspect.id == 'attacks') then
local facet = H.get_child(aspect, 'facet')
if facet then
filter_own = H.get_child(facet, 'filter_own')
filter_enemy = H.get_child(facet, 'filter_enemy')
end
end
end
end
local leader = wesnoth.get_units {
side = wesnoth.current.side,
canrecruit = 'yes',
{ "and", filter_own }
}[1]
if (not leader) then return 0 end
if (leader.attacks_left == 0) or (not H.get_child(leader.__cfg, 'attack')) then return 0 end
local excluded_enemies_map = LS.create()
-- Exclude enemies not matching [filter_enemy]
if filter_enemy then
local excluded_enemies = wesnoth.get_units {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } },
{ "not", filter_enemy }
}
for _,e in ipairs(excluded_enemies) do
excluded_enemies_map:insert(e.x, e.y)
end
end
-- Exclude hidden enemies, except if attack_hidden_enemies=yes is set in [micro_ai] tag
if (not cfg.attack_hidden_enemies) then
local hidden_enemies = wesnoth.get_units {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } },
{ "filter_vision", { side = wesnoth.current.side, visible = 'no' } }
}
for _,e in ipairs(hidden_enemies) do
excluded_enemies_map:insert(e.x, e.y)
end
end
local aggression = ai.get_aggression()
if (aggression > 1) then aggression = 1 end
local own_value_weight = 1. - aggression
-- Get the locations to be avoided
local avoid_map = FAU.get_avoid_map(cfg)
-- Enemy power and number maps
-- Currently, the power is simply the summed hitpoints of all enemies that
-- can get to a hex
local enemies = wesnoth.get_units {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } }
}
local enemy_power_map = LS.create()
local enemy_number_map = LS.create()
for _,enemy in ipairs(enemies) do
-- Only need to consider enemies that are close enough
if (H.distance_between(leader.x, leader.y, enemy.x, enemy.y) <= (enemy.max_moves + leader.max_moves + 1)) then
enemy_power = enemy.hitpoints
local old_moves = enemy.moves
enemy.moves = enemy.max_moves
local reach = wesnoth.find_reach(enemy)
enemy.moves = old_moves
for _,loc in ipairs(reach) do
enemy_power_map:insert(
loc[1], loc[2],
(enemy_power_map:get(loc[1], loc[2]) or 0) + enemy_power
)
enemy_number_map:insert(
loc[1], loc[2],
(enemy_number_map:get(loc[1], loc[2]) or 0) + 1
)
end
end
end
local leader_info = FAU.get_unit_info(leader, self.data.gamedata)
local leader_copy = FAU.get_unit_copy(leader.id, self.data.gamedata)
local attacks = AH.get_attacks({ leader }, { include_occupied = cfg.include_occupied_attack_hexes })
if (#attacks > 0) then
local max_rating, best_target, best_dst = -9e99
for _,attack in ipairs(attacks) do
if (not excluded_enemies_map:get(attack.target.x, attack.target.y))
and (not avoid_map:get(attack.dst.x, attack.dst.y))
then
-- First check if the threat against the leader at this hex
-- is acceptable
local acceptable_attack = true
for xa,ya in H.adjacent_tiles(attack.dst.x, attack.dst.y) do
local enemy_power = enemy_power_map:get(xa, ya) or 0
local enemy_number = enemy_number_map:get(xa, ya) or 0
if (enemy_power * leader_weight > leader.hitpoints)
or (enemy_number > leader_attack_max_units)
then
acceptable_attack = false
break
end
end
if acceptable_attack then
local target = wesnoth.get_unit(attack.target.x, attack.target.y)
local target_info = FAU.get_unit_info(target, self.data.gamedata)
local att_stat, def_stat = FAU.battle_outcome(
leader_copy, target, { attack.dst.x, attack.dst.y },
leader_info, target_info, self.data.gamedata, self.data.move_cache
)
local rating, attacker_rating, defender_rating, extra_rating = FAU.attack_rating(
{ leader_info }, target_info, { { attack.dst.x, attack.dst.y } },
{ att_stat }, def_stat, self.data.gamedata,
{
own_value_weight = own_value_weight,
leader_weight = cfg.leader_weight
}
)
acceptable_attack = FAU.is_acceptable_attack(attacker_rating, defender_rating, own_value_weight)
--std_print(leader.id, target.id, rating, attacker_rating, defender_rating, extra_rating)
--std_print(' -->' , own_value_weight, defender_rating/attacker_rating, acceptable_attack)
if acceptable_attack and (rating > max_rating) then
max_rating, best_target, best_dst = rating, target, attack.dst
end
end
end
end
if best_target then
self.data.leader = leader
self.data.fast_target, self.data.fast_dst = best_target, best_dst
return cfg.ca_score
end
end
return 0
end
function ca_fast_combat_leader:execution(ai, cfg, self)
local leader = self.data.leader
AH.movefull_outofway_stopunit(ai, leader, self.data.fast_dst.x, self.data.fast_dst.y)
if (not leader) or (not leader.valid) then return end
if (not self.data.fast_target) or (not self.data.fast_target.valid) then return end
if (H.distance_between(leader.x, leader.y, self.data.fast_target.x, self.data.fast_target.y) ~= 1) then return end
AH.checked_attack(ai, leader, self.data.fast_target)
self.data.leader = nil
end
return ca_fast_combat_leader

View file

@ -461,13 +461,14 @@ function wesnoth.wml_actions.micro_ai(cfg)
elseif (cfg.ai_type == 'fast_ai') then
optional_keys = {
"attack_hidden_enemies", "avoid", "dungeon_mode", "filter", "filter_second",
"include_occupied_attack_hexes", "leader_weight", "move_cost_factor",
"include_occupied_attack_hexes", "leader_attack_max_units", "leader_weight", "move_cost_factor",
"weak_units_first", "skip_combat_ca", "skip_move_ca"
}
CA_parms = {
ai_id = 'mai_fast',
{ ca_id = 'combat', location = CA_path .. 'ca_fast_combat.lua', score = 100000 },
{ ca_id = 'move', location = CA_path .. 'ca_fast_move.lua', score = 20000 }
{ ca_id = 'move', location = CA_path .. 'ca_fast_move.lua', score = 20000 },
{ ca_id = 'combat_leader', location = CA_path .. 'ca_fast_combat_leader.lua', score = 19900 }
}
-- Also need to delete/add some default CAs
@ -533,9 +534,8 @@ function wesnoth.wml_actions.micro_ai(cfg)
}
else
for i,parm in ipairs(CA_parms) do
if (parm.ca_id == 'combat') then
if (parm.ca_id == 'combat') or (parm.ca_id == 'combat_leader') then
table.remove(CA_parms, i)
break
end
end
end