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:
parent
620da22082
commit
34956ac15d
6 changed files with 142 additions and 86 deletions
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue