Skip to content

Commit

Permalink
Micro AIs: correctly deal with hidden and petrified enemies
Browse files Browse the repository at this point in the history
Previously, the Micro AI behavior was inconsistent at best when it came
to dealing with these units and could even result in AI errors when an
AI unit was ambushed or a petrified unit was in the way of a move. Now,
both types of units are properly "ignored" and the AI moves have been
made robust against unexpected events such as ambushes. Incidentally,
the latter also makes the AI more robust against WML events doing
things the AI cannot know about (such as removing units).
  • Loading branch information
mattsc committed Oct 17, 2016
1 parent 545800f commit b302289
Show file tree
Hide file tree
Showing 32 changed files with 231 additions and 299 deletions.
61 changes: 32 additions & 29 deletions data/ai/micro_ais/cas/ca_assassin_move.lua
Expand Up @@ -53,39 +53,43 @@ function ca_assassin_move:execution(cfg)
local units, target = get_units_target(cfg)
local unit = units[1]

local enemies = wesnoth.get_units {
local enemies = AH.get_visible_units(wesnoth.current.side, {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } },
{ "not", H.get_child(cfg, "filter_second") }
}
})

-- Maximum damage the enemies can theoretically do for all hexes they can attack
-- Note: petrified enemies need to be included for the blocked hexes rating below,
-- but need to be excluded from the damage rating
local enemy_damage_map = LS.create()
for _,enemy in ipairs(enemies) do
-- Need to "move" enemy next to unit for attack calculation
-- Do this with a unit copy, so that no actual unit has to be moved
local enemy_copy = wesnoth.copy_unit(enemy)

-- First get the reach of the enemy with full moves though
enemy_copy.moves = enemy_copy.max_moves
local reach = wesnoth.find_reach(enemy_copy, { ignore_units = true })

enemy_copy.x = unit.x
enemy_copy.y = unit.y + 1 -- this even works at map border

local _, _, att_weapon, _ = wesnoth.simulate_combat(enemy_copy, unit)
local max_damage = att_weapon.damage * att_weapon.num_blows

local unit_damage_map = LS.create()
for _,loc in ipairs(reach) do
unit_damage_map:insert(loc[1], loc[2], max_damage)
for xa,ya in H.adjacent_tiles(loc[1], loc[2]) do
unit_damage_map:insert(xa, ya, max_damage)
if (not enemy.status.petrified) then
-- Need to "move" enemy next to unit for attack calculation
-- Do this with a unit copy, so that no actual unit has to be moved
local enemy_copy = wesnoth.copy_unit(enemy)

-- First get the reach of the enemy with full moves though
enemy_copy.moves = enemy_copy.max_moves
local reach = wesnoth.find_reach(enemy_copy, { ignore_units = true })

enemy_copy.x = unit.x
enemy_copy.y = unit.y + 1 -- this even works at map border

local _, _, att_weapon, _ = wesnoth.simulate_combat(enemy_copy, unit)
local max_damage = att_weapon.damage * att_weapon.num_blows

local unit_damage_map = LS.create()
for _,loc in ipairs(reach) do
unit_damage_map:insert(loc[1], loc[2], max_damage)
for xa,ya in H.adjacent_tiles(loc[1], loc[2]) do
unit_damage_map:insert(xa, ya, max_damage)
end
end
end

enemy_damage_map:union_merge(unit_damage_map, function(x, y, v1, v2)
return (v1 or 0) + v2
end)
enemy_damage_map:union_merge(unit_damage_map, function(x, y, v1, v2)
return (v1 or 0) + v2
end)
end
end

-- Penalties for damage by enemies
Expand All @@ -107,12 +111,11 @@ function ca_assassin_move:execution(cfg)
enemy_rating_map:insert(enemy.x, enemy.y, (enemy_rating_map:get(enemy.x, enemy.y) or 0) + 100)

-- Hexes adjacent to enemies get max_moves penalty
-- except if AI unit is skirmisher or enemy is level 0
-- except if AI unit is skirmisher or enemy is level 0 or is petrified
local zoc_active = (not is_skirmisher)

if zoc_active then
local level = enemy.level
if (level == 0) then zoc_active = false end
if (enemy.level == 0) or enemy.status.petrified then zoc_active = false end
end

if zoc_active then
Expand Down Expand Up @@ -149,7 +152,7 @@ function ca_assassin_move:execution(cfg)
local sub_path, sub_cost = wesnoth.find_path(unit, path[i][1], path[i][2])
if sub_cost <= unit.moves then
local unit_in_way = wesnoth.get_unit(path[i][1], path[i][2])
if not unit_in_way then
if (not AH.is_visible_unit(wesnoth.current.side, unit_in_way)) then
farthest_hex = path[i]
end
else
Expand Down
16 changes: 9 additions & 7 deletions data/ai/micro_ais/cas/ca_big_animals.lua
Expand Up @@ -27,11 +27,13 @@ function ca_big_animals:execution(cfg)
local avoid_tag = H.get_child(cfg, "avoid_unit")
local avoid_map = LS.create()
if avoid_tag then
avoid_map = LS.of_pairs(wesnoth.get_locations { radius = 1,
{ "filter", { { "and", avoid_tag },
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } }
} }
})
local enemies_to_be_avoided = AH.get_attackable_enemies(avoid_tag)
for _,enemy in ipairs(enemies_to_be_avoided) do
avoid_map:insert(enemy.x, enemy.y)
for xa,ya in H.adjacent_tiles(enemy.x, enemy.y) do
avoid_map:insert(xa, ya)
end
end
end

local goal = MAIUV.get_mai_unit_variables(unit, cfg.ai_id)
Expand Down Expand Up @@ -64,7 +66,7 @@ function ca_big_animals:execution(cfg)
local enemy_hp = 500
for xa,ya in H.adjacent_tiles(x, y) do
local enemy = wesnoth.get_unit(xa, ya)
if enemy and wesnoth.is_enemy(enemy.side, wesnoth.current.side) then
if AH.is_attackable_enemy(enemy) then
if (enemy.hitpoints < enemy_hp) then enemy_hp = enemy.hitpoints end
end
end
Expand Down Expand Up @@ -96,7 +98,7 @@ function ca_big_animals:execution(cfg)
local min_hp, target = 9e99
for xa,ya in H.adjacent_tiles(unit.x, unit.y) do
local enemy = wesnoth.get_unit(xa, ya)
if enemy and wesnoth.is_enemy(enemy.side, wesnoth.current.side) then
if AH.is_attackable_enemy(enemy) then
if (enemy.hitpoints < min_hp) then
min_hp, target = enemy.hitpoints, enemy
end
Expand Down
5 changes: 1 addition & 4 deletions data/ai/micro_ais/cas/ca_bottleneck_attack.lua
Expand Up @@ -14,10 +14,7 @@ function ca_bottleneck_attack:evaluation(cfg, data)

local max_rating, best_attacker, best_target, best_weapon = -9e99
for _,attacker in ipairs(attackers) do
local targets = wesnoth.get_units {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } },
{ "filter_adjacent", { id = attacker.id } }
}
local targets = AH.get_attackable_enemies { { "filter_adjacent", { id = attacker.id } } }

for _,target in ipairs(targets) do
local n_weapon = 0
Expand Down
28 changes: 13 additions & 15 deletions data/ai/micro_ais/cas/ca_bottleneck_move.lua
Expand Up @@ -185,7 +185,7 @@ local function bottleneck_move_out_of_way(unit_in_way, data)

local reach = wesnoth.find_reach(unit_in_way)

local all_units = wesnoth.get_units()
local all_units = AH.get_visible_units(wesnoth.current.side)
local occ_hexes = LS:create()
for _,unit in ipairs(all_units) do
occ_hexes:insert(unit.x, unit.y)
Expand Down Expand Up @@ -309,14 +309,15 @@ function ca_bottleneck_move:evaluation(cfg, data)
if (not best_move_away) then current_rating_map:insert(unit.x, unit.y, 20000) end
end

local enemies = AH.get_live_units {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } }
}
local enemies = AH.get_attackable_enemies()
local attacks = {}
for _,enemy in ipairs(enemies) do
for xa,ya in H.adjacent_tiles(enemy.x, enemy.y) do
if data.BD_is_my_territory:get(xa, ya) then
local unit_in_way = wesnoth.get_unit(xa, ya)
if (not AH.is_visible_unit(wesnoth.current.side, unit_in_way)) then
unit_in_way = nil
end
local data = { x = xa, y = ya,
defender = enemy,
defender_level = enemy.level,
Expand All @@ -330,10 +331,10 @@ function ca_bottleneck_move:evaluation(cfg, data)
-- Get a map of the allies, as hexes occupied by allied units count as
-- reachable, but must be excluded. This could also be done below by
-- using bottleneck_move_out_of_way(), but this is much faster
local allies = AH.get_live_units {
local allies = AH.get_visible_units(wesnoth.current.side, {
{ "filter_side", { { "allied_with", { side = wesnoth.current.side } } } },
{ "not", { side = wesnoth.current.side } }
}
})
local allies_map = LS.create()
for _,ally in ipairs(allies) do
allies_map:insert(ally.x, ally.y)
Expand Down Expand Up @@ -434,6 +435,9 @@ function ca_bottleneck_move:evaluation(cfg, data)
local unit_in_way = wesnoth.get_units { x = best_hex[1], y = best_hex[2],
{ "not", { id = best_unit.id } }
}[1]
if (not AH.is_visible_unit(wesnoth.current.side, unit_in_way)) then
unit_in_way = nil
end

if unit_in_way then
best_hex = bottleneck_move_out_of_way(unit_in_way, data)
Expand Down Expand Up @@ -462,15 +466,9 @@ function ca_bottleneck_move:execution(cfg, data)
AH.checked_stopunit_moves(ai, unit)
end
else
if (data.BD_unit.x ~= data.BD_hex[1]) or (data.BD_unit.y ~= data.BD_hex[2]) then
-- Don't want full move, as this might be stepping out of the way
AH.checked_move(ai, data.BD_unit, data.BD_hex[1], data.BD_hex[2])
end
if (not data.BD_unit) or (not data.BD_unit.valid) then return end

if data.BD_level_up_defender then
AH.checked_attack(ai, data.BD_unit, data.BD_level_up_defender, data.BD_level_up_weapon)
end
-- Don't want full move, as this might be stepping out of the way
local cfg = { partial_move = true, weapon = data.BD_level_up_weapon }
AH.robust_move_and_attack(ai, data.BD_unit, data.BD_hex, data.BD_level_up_defender, cfg)
end

-- Now delete almost everything
Expand Down
11 changes: 7 additions & 4 deletions data/ai/micro_ais/cas/ca_coward.lua
Expand Up @@ -25,9 +25,10 @@ function ca_coward:execution(cfg)
local filter_second =
H.get_child(cfg, "filter_second")
or { { "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } } }
local enemies = wesnoth.get_units {
local enemies = AH.get_live_units {
{ "and", filter_second },
{ "filter_location", { x = coward.x, y = coward.y, radius = cfg.distance } }
{ "filter_location", { x = coward.x, y = coward.y, radius = cfg.distance } },
{ "filter_vision", { side = wesnoth.current.side, visible = 'yes' } }
}

-- If no enemies are close: keep unit from doing anything and exit
Expand All @@ -38,8 +39,10 @@ function ca_coward:execution(cfg)

for i,hex in ipairs(reach) do
-- Only consider unoccupied hexes
local occ_hex = wesnoth.get_units { x = hex[1], y = hex[2], { "not", { id = coward.id } } }[1]
if not occ_hex then
local unit_in_way = wesnoth.get_unit(hex[1], hex[2])
if (not AH.is_visible_unit(wesnoth.current.side, unit_in_way))
or (unit_in_way == coward)
then
local rating = 0
for _,enemy in ipairs(enemies) do
local dist = H.distance_between(hex[1], hex[2], enemy.x, enemy.y)
Expand Down
14 changes: 7 additions & 7 deletions data/ai/micro_ais/cas/ca_fast_attack_utils.lua
Expand Up @@ -93,20 +93,20 @@ local function get_attack_filter_from_aspect(aspect, which, data, is_leader)
local filter = loadstring(aspect.code)(nil, H.get_child(aspect, 'args'), data)
if (type(filter[which]) == 'function') then
temporary_attacks_filter_fcn = filter[which]
local units = wesnoth.get_units(attack_filter(which, {
local units = AH.get_live_units(attack_filter(which, {
lua_function = 'temporary_attacks_filter_fcn'
}, is_leader))
temporary_attacks_filter_fcn = nil
return units
else
return wesnoth.get_units(attack_filter(which, filter[which], is_leader))
return AH.get_live_units(attack_filter(which, filter[which], is_leader))
end
else -- Standard attacks aspect (though not name=standard_aspect)
--print("Found standard aspect")
return wesnoth.get_units(attack_filter(which,
return AH.get_live_units(attack_filter(which,
H.get_child(aspect, 'filter_' .. which), is_leader))
end
return wesnoth.get_units(attack_filter(which, {}, is_leader))
return AH.get_live_units(attack_filter(which, {}, is_leader))
end

function ca_fast_attack_utils.get_attackers(data, which)
Expand Down Expand Up @@ -147,12 +147,12 @@ function ca_fast_attack_utils.test_attacks(my_ai, times)
if (aspect.id == 'attacks') then
local facet = H.get_child(aspect, 'facet')
if facet then
wesnoth.get_units{
AH.get_live_units{
side = wesnoth.current.side,
canrecruit = false,
{ "and", H.get_child(facet, 'filter_own') }
}
wesnoth.get_units{
AH.get_live_units{
side = wesnoth.current.side,
canrecruit = false,
{ "and", H.get_child(facet, 'filter_enemy') }
Expand Down Expand Up @@ -189,7 +189,7 @@ function ca_fast_attack_utils.gamedata_setup()
-- Only uses one leader per side right now, but only used for finding direction
-- of move -> sufficient for this.
gamedata.leaders = {}
for _,unit_proxy in ipairs(wesnoth.get_units { canrecruit = 'yes' }) do
for _,unit_proxy in ipairs(AH.get_live_units { canrecruit = 'yes' }) do
gamedata.leaders[unit_proxy.side] = { unit_proxy.x, unit_proxy.y, id = unit_proxy.id }
end

Expand Down
16 changes: 4 additions & 12 deletions data/ai/micro_ais/cas/ca_fast_combat.lua
Expand Up @@ -22,14 +22,14 @@ function ca_fast_combat:evaluation(cfg, data)
excluded_enemies = FAU.get_attackers(data, "enemy")
else
if (not data.fast_combat_units) or (not data.fast_combat_units[1]) then
data.fast_combat_units = wesnoth.get_units(
data.fast_combat_units = AH.get_live_units(
FAU.build_attack_filter("own", filter_own)
)
if (not data.fast_combat_units[1]) then return 0 end
units_sorted = false
end
if filter_enemy then
excluded_enemies = wesnoth.get_units(
excluded_enemies = AH.get_live_units(
FAU.build_attack_filter("enemy", filter_enemy)
)
end
Expand All @@ -55,7 +55,7 @@ function ca_fast_combat:evaluation(cfg, data)

-- 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 {
local hidden_enemies = AH.get_live_units {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } },
{ "filter_vision", { side = wesnoth.current.side, visible = 'no' } }
}
Expand Down Expand Up @@ -126,15 +126,7 @@ function ca_fast_combat:evaluation(cfg, data)
end

function ca_fast_combat:execution(cfg, data)
local unit = data.fast_combat_units[data.fast_combat_unit_i]
AH.movefull_outofway_stopunit(ai, unit, data.fast_dst.x, data.fast_dst.y)

if (not unit) or (not unit.valid) then return end
if (not data.fast_target) or (not data.fast_target.valid) then return end
if (H.distance_between(unit.x, unit.y, data.fast_target.x, data.fast_target.y) ~= 1) then return end

AH.checked_attack(ai, unit, data.fast_target)

AH.robust_move_and_attack(ai, data.fast_combat_units[data.fast_combat_unit_i], data.fast_dst, data.fast_target)
data.fast_combat_units[data.fast_combat_unit_i] = nil
end

Expand Down
18 changes: 5 additions & 13 deletions data/ai/micro_ais/cas/ca_fast_combat_leader.lua
Expand Up @@ -26,12 +26,12 @@ function ca_fast_combat_leader:evaluation(cfg, data)
if (not leader) then return 0 end
excluded_enemies = FAU.get_attackers(data, "enemy")
else
leader = wesnoth.get_units(
leader = AH.get_live_units(
FAU.build_attack_filter("leader", filter_own)
)[1]
if (not leader) then return 0 end
if filter_enemy then
excluded_enemies = wesnoth.get_units(
excluded_enemies = AH.get_live_units(
FAU.build_attack_filter("enemy", filter_enemy)
)
end
Expand All @@ -50,7 +50,7 @@ function ca_fast_combat_leader:evaluation(cfg, data)

-- 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 {
local hidden_enemies = AH.get_live_units {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } },
{ "filter_vision", { side = wesnoth.current.side, visible = 'no' } }
}
Expand All @@ -70,7 +70,7 @@ function ca_fast_combat_leader:evaluation(cfg, data)
-- 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 {
local enemies = AH.get_live_units {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } }
}

Expand Down Expand Up @@ -184,15 +184,7 @@ function ca_fast_combat_leader:evaluation(cfg, data)
end

function ca_fast_combat_leader:execution(cfg, data)
local leader = data.leader
AH.movefull_outofway_stopunit(ai, leader, data.fast_dst.x, data.fast_dst.y)

if (not leader) or (not leader.valid) then return end
if (not data.fast_target) or (not data.fast_target.valid) then return end
if (H.distance_between(leader.x, leader.y, data.fast_target.x, data.fast_target.y) ~= 1) then return end

AH.checked_attack(ai, leader, data.fast_target)

AH.robust_move_and_attack(ai, data.leader, data.fast_dst, data.fast_target)
data.leader = nil
end

Expand Down

0 comments on commit b302289

Please sign in to comment.