AI: ensure all CAs respect [avoid] tags

This is for the candidate actions that were merged into the default AI from the former Experimental AI.
This commit is contained in:
mattsc 2019-11-29 07:05:24 -08:00
parent 620da22082
commit 34956ac15d
6 changed files with 142 additions and 86 deletions

View file

@ -6,7 +6,7 @@ local M = wesnoth.map
local CS_leader_score
-- Note that leader_target is also needed by the recruiting CA, so it must be stored in 'data'
local function get_reachable_enemy_leaders(unit)
local function get_reachable_enemy_leaders(unit, avoid_map)
-- We're cheating a little here and also find hidden enemy leaders. That's
-- because a human player could make a pretty good educated guess as to where
-- the enemy leaders are likely to be while the AI does not know how to do that.
@ -14,10 +14,13 @@ local function get_reachable_enemy_leaders(unit)
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
}
local enemy_leaders = {}
for j,e in ipairs(potential_enemy_leaders) do
local path, cost = wesnoth.find_path(unit, e.x, e.y, { ignore_units = true, viewing_side = 0 })
if cost < AH.no_path then
table.insert(enemy_leaders, e)
for _,e in ipairs(potential_enemy_leaders) do
-- Cannot use AH.find_path_with_avoid() here as there might be enemies all around the enemy leader
if (not avoid_map:get(e.x, e.y)) then
local path, cost = wesnoth.find_path(unit, e.x, e.y, { ignore_units = true, viewing_side = 0 })
if cost < AH.no_path then
table.insert(enemy_leaders, e)
end
end
end
@ -50,12 +53,17 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
local cheapest_unit_cost = AH.get_cheapest_recruit_cost()
local avoid_map = AH.get_avoid_map(ai, nil, true)
if data.leader_target and wesnoth.sides[wesnoth.current.side].gold >= cheapest_unit_cost then
-- make sure move is still valid
local next_hop = AH.next_hop(leader, data.leader_target[1], data.leader_target[2])
local path, cost = AH.find_path_with_avoid(leader, data.leader_target[1], data.leader_target[2], avoid_map)
local next_hop = AH.next_hop(leader, nil, nil, { path = path, avoid_map = avoid_map })
if next_hop and next_hop[1] == data.leader_target[1]
and next_hop[2] == data.leader_target[2] then
return CS_leader_score
else
data.leader_target = nil
end
end
@ -81,13 +89,13 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
return 0
end
local enemy_leaders = get_reachable_enemy_leaders(leader)
local enemy_leaders = get_reachable_enemy_leaders(leader, avoid_map)
-- Look for the best keep
local best_score, best_loc, best_turns = 0, {}, 3
local best_score, best_loc, best_turns, best_path = 0, {}, 3
for i,loc in ipairs(keeps) do
-- Only consider keeps within 2 turns movement
local path, cost = wesnoth.find_path(leader, loc[1], loc[2])
local path, cost = AH.find_path_with_avoid(leader, loc[1], loc[2], avoid_map)
local score = 0
-- Prefer closer keeps to enemy
local turns = math.ceil(cost/leader.max_moves)
@ -101,6 +109,7 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
best_score = score
best_loc = loc
best_turns = turns
best_path = path
end
end
end
@ -130,7 +139,7 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
end
if best_score > 0 then
local next_hop = AH.next_hop(leader, best_loc[1], best_loc[2])
local next_hop = AH.next_hop(leader, nil, nil, { path = best_path, avoid_map = avoid_map })
if next_hop and ((next_hop[1] ~= leader.x) or (next_hop[2] ~= leader.y)) then
-- See if there is a nearby village that can be captured without delaying progress
@ -138,12 +147,12 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
{ "and", { x = next_hop[1], y = next_hop[2], radius = leader.max_moves }},
owner_side = 0 })
for i,loc in ipairs(close_villages) do
local path_village, cost_village = wesnoth.find_path(leader, loc[1], loc[2])
local path_village, cost_village = AH.find_path_with_avoid(leader, loc[1], loc[2], avoid_map)
if cost_village <= leader.moves then
local dummy_leader = leader:clone()
dummy_leader.x = loc[1]
dummy_leader.y = loc[2]
local path_keep, cost_keep = wesnoth.find_path(dummy_leader, best_loc[1], best_loc[2])
local path_keep, cost_keep = wesnoth.find_path(dummy_leader, best_loc[1], best_loc[2], avoid_map)
local turns_from_keep = math.ceil(cost_keep/leader.max_moves)
if turns_from_keep < best_turns
or (turns_from_keep == 1 and wesnoth.sides[wesnoth.current.side].gold < cheapest_unit_cost)

View file

@ -2,6 +2,7 @@
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local BC = wesnoth.require "ai/lua/battle_calcs.lua"
local LS = wesnoth.require "location_set"
local M = wesnoth.map
local GV_unit, GV_village
@ -25,8 +26,15 @@ function ca_grab_villages:evaluation(cfg, data, filter_own)
local enemies = AH.get_attackable_enemies()
local villages = wesnoth.get_villages()
-- Just in case:
local avoid_map = LS.of_pairs(ai.aspects.avoid)
local all_villages, villages = wesnoth.get_villages(), {}
for _,village in ipairs(all_villages) do
if (not avoid_map:get(village[1], village[2])) then
table.insert(villages, village)
end
end
if (not villages[1]) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0

View file

@ -3,6 +3,7 @@
-- only kicks in when the AI would do nothing else. It prevents the AI from
-- being inactive on maps without enemy leaders and villages.
local H = wesnoth.require "helper"
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local MTAE_unit, MTAE_destination
@ -25,41 +26,52 @@ function ca_move_to_any_enemy:evaluation(cfg, data, filter_own)
return 0
end
local avoid_map = AH.get_avoid_map(ai, nil, true)
-- In principle we don't even need to pass avoid_map here, as the loop below also
-- checks this, but we might as well eliminate unreachable enemies right away
local enemies = AH.get_attackable_enemies({}, wesnoth.current.sude, { avoid_map = avoid_map })
local unit, destination
-- Find a unit that has a path to an space close to an enemy
-- 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.
for i,u in ipairs(units) do
local target = AH.get_closest_enemy({u.x, u.y})
if target then
unit = u
local x, y = wesnoth.find_vacant_tile(target.x, target.y)
destination = AH.next_hop(unit, x, y)
if (destination[1] == unit.x) and (destination[2] == unit.y) then
destination = nil
local best_cost, best_path, best_enemy = AH.no_path
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)
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 destination then
break
if best_enemy 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
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 1000
end
end
end
if (not destination) then
-- No path was found
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
MTAE_destination = destination
MTAE_unit = unit
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 1000
return 0
end
function ca_move_to_any_enemy:execution(cfg, data)
if AH.print_exec() then AH.print_ts(' Executing move_to_any_enemy CA') end
AH.checked_move(ai, MTAE_unit, MTAE_destination[1], MTAE_destination[2])
AH.checked_move_full(ai, MTAE_unit, MTAE_destination[1], MTAE_destination[2])
MTAE_unit, MTAE_destination = nil,nil
end

View file

@ -1,6 +1,7 @@
------- Retreat CA --------------
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local LS = wesnoth.require "location_set"
local R = wesnoth.require "ai/lua/retreat.lua"
local retreat_unit, retreat_loc
@ -15,7 +16,8 @@ function ca_retreat_injured:evaluation(cfg, data, filter_own)
side = wesnoth.current.side,
{ "and", filter_own }
}, true)
local unit, loc = R.retreat_injured_units(units)
local avoid_map = LS.of_pairs(ai.aspects.avoid)
local unit, loc = R.retreat_injured_units(units, avoid_map)
if unit then
retreat_unit = unit
retreat_loc = loc

View file

@ -1,8 +1,12 @@
------- Village Hunt CA --------------
-- Give extra priority to seeking villages if we have less than our share
-- our share is defined as being slightly more than the total/the number of sides
-- Give extra priority to seeking villages if we have less than our share.
-- Our share is defined as being slightly more than the total/the number of sides,
-- but only in the area not prohibited by an [avoid] directive.
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local LS = wesnoth.require "location_set"
local VH_unit, VH_dst = {}, {}
local ca_village_hunt = {}
@ -10,22 +14,37 @@ function ca_village_hunt:evaluation(cfg, data, filter_own)
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'village_hunt'
if AH.print_eval() then AH.print_ts(' - Evaluating village_hunt CA:') end
local villages = wesnoth.get_villages()
local avoid_map = LS.of_pairs(ai.aspects.avoid)
if not villages[1] then
local all_villages, villages = wesnoth.get_villages(), {}
for _,village in ipairs(all_villages) do
if (not avoid_map:get(village[1], village[2])) then
table.insert(villages, village)
end
end
if (not villages[1]) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
local my_villages = wesnoth.get_villages { owner_side = wesnoth.current.side }
local n_my_villages, n_allied_villages = 0, 0
for _,village in ipairs(villages) do
local owner = wesnoth.get_village_owner(village[1], village[2]) or -1
if (owner == wesnoth.current.side) then
n_my_villages = n_my_villages + 1
end
if (owner ~= -1) and (not wesnoth.sides.is_enemy(owner, wesnoth.current.side)) then
n_allied_villages = n_allied_villages + 1
end
end
if #my_villages > #villages / #wesnoth.sides then
if (n_my_villages > #villages / #wesnoth.sides) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
local allied_villages = wesnoth.get_villages { {"filter_owner", { {"ally_of", { side = wesnoth.current.side }} }} }
if #allied_villages == #villages then
if (n_allied_villages == #villages) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
@ -36,7 +55,32 @@ function ca_village_hunt:evaluation(cfg, data, filter_own)
{ "and", filter_own }
}, true)
if not units[1] then
if (not units[1]) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
local avoid_map = AH.get_avoid_map(ai, nil, true)
VH_unit = nil
for _,unit in ipairs(units) do
local best_cost = AH.no_path
for i,v in ipairs(villages) do
if not wesnoth.match_location(v[1], v[2], { {"filter_owner", { {"ally_of", { side = wesnoth.current.side }} }} }) then
local path, cost = AH.find_path_with_avoid(unit, v[1], v[2], avoid_map)
if (cost < best_cost) then
local dst = AH.next_hop(unit, nil, nil, { path = path, avoid_map = avoid_map })
if (dst[1] ~= unit.x) or (dst[2] ~= unit.y) then
best_cost = cost
VH_unit = unit
VH_dst = dst
end
end
end
end
end
if (not VH_unit) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
@ -46,31 +90,10 @@ function ca_village_hunt:evaluation(cfg, data, filter_own)
end
function ca_village_hunt:execution(cfg, data, filter_own)
local unit = AH.get_units_with_moves({
side = wesnoth.current.side,
canrecruit = 'no',
{ "and", filter_own }
}, true)[1]
if AH.print_exec() then AH.print_ts(' Executing village_hunt CA') end
local villages = wesnoth.get_villages()
local best_cost, target = AH.no_path
for i,v in ipairs(villages) do
if not wesnoth.match_location(v[1], v[2], { {"filter_owner", { {"ally_of", { side = wesnoth.current.side }} }} }) then
local path, cost = wesnoth.find_path(unit, v[1], v[2], { ignore_units = true, max_cost = best_cost })
if cost < best_cost then
target = v
best_cost = cost
end
end
end
if target then
local x, y = wesnoth.find_vacant_tile(target[1], target[2], unit)
local dest = AH.next_hop(unit, x, y)
AH.checked_move(ai, unit, dest[1], dest[2])
end
AH.checked_move_full(ai, VH_unit, VH_dst[1], VH_dst[2])
VH_unit, VH_dst = nil, nil
end
return ca_village_hunt

View file

@ -33,7 +33,7 @@ end
-- Given a set of units, return one from the set that should retreat and the location to retreat to
-- Return nil if no unit needs to retreat
function retreat_functions.retreat_injured_units(units)
function retreat_functions.retreat_injured_units(units, avoid_map)
-- Split units into those that regenerate and those that do not
local regen, regen_amounts, non_regen = {}, {}, {}
for i,u in ipairs(units) do
@ -60,7 +60,7 @@ function retreat_functions.retreat_injured_units(units)
-- First we retreat non-regenerating units to healing terrain, if they can get to a safe location
local unit_nr, loc_nr, threat_nr
if non_regen[1] then
unit_nr, loc_nr, threat_nr = retreat_functions.get_retreat_injured_units(non_regen, {})
unit_nr, loc_nr, threat_nr = retreat_functions.get_retreat_injured_units(non_regen, {}, avoid_map)
if unit_nr and (threat_nr == 0) then
return unit_nr, loc_nr, threat_nr
end
@ -69,7 +69,7 @@ function retreat_functions.retreat_injured_units(units)
-- Then we retreat regenerating units to terrain with high defense, if they can get to a safe location
local unit_r, loc_r, threat_r
if regen[1] then
unit_r, loc_r, threat_r = retreat_functions.get_retreat_injured_units(regen, regen_amounts)
unit_r, loc_r, threat_r = retreat_functions.get_retreat_injured_units(regen, regen_amounts, avoid_map)
if unit_r and (threat_r == 0) then
return unit_r, loc_r, threat_r
end
@ -118,7 +118,7 @@ function retreat_functions.get_healing_locations()
return healing_locs
end
function retreat_functions.get_retreat_injured_units(healees, regen_amounts)
function retreat_functions.get_retreat_injured_units(healees, regen_amounts, avoid_map)
-- Only retreat to safe locations
local enemies = AH.get_attackable_enemies()
local enemy_attack_map = BC.get_attack_map(enemies)
@ -134,20 +134,22 @@ function retreat_functions.get_retreat_injured_units(healees, regen_amounts)
-- Unit cannot self heal, make the terrain do it for us if possible
local location_subset = {}
for j,loc in ipairs(possible_locations) do
local heal_amount = wesnoth.get_terrain_info(wesnoth.get_terrain(loc[1], loc[2])).healing or 0
if heal_amount == true then
-- handle deprecated syntax
-- TODO: remove this when removed from game
heal_amount = 8
if (not avoid_map) or (not avoid_map:get(loc[1], loc[2])) then
local heal_amount = wesnoth.get_terrain_info(wesnoth.get_terrain(loc[1], loc[2])).healing or 0
if heal_amount == true then
-- handle deprecated syntax
-- TODO: remove this when removed from game
heal_amount = 8
end
local curing = 0
if heal_amount > 0 then
curing = 2
end
local healer_values = healing_locs:get(loc[1], loc[2]) or {0, 0}
heal_amount = math.max(heal_amount, healer_values[1])
curing = math.max(curing, healer_values[2])
table.insert(location_subset, {loc[1], loc[2], heal_amount, curing})
end
local curing = 0
if heal_amount > 0 then
curing = 2
end
local healer_values = healing_locs:get(loc[1], loc[2]) or {0, 0}
heal_amount = math.max(heal_amount, healer_values[1])
curing = math.max(curing, healer_values[2])
table.insert(location_subset, {loc[1], loc[2], heal_amount, curing})
end
possible_locations = location_subset