Merge pull request #5573 from mattsc/ca_retreat_injured

AI: Improve behavior of the retreat_injured CA
This commit is contained in:
mattsc 2021-03-03 08:22:21 -08:00 committed by GitHub
commit 7e8cedaea8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 179 additions and 48 deletions

View file

@ -12,7 +12,7 @@ function ca_retreat_injured:evaluation(cfg, data, filter_own)
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'retreat_injured'
if AH.print_eval() then AH.print_ts(' - Evaluating retreat_injured CA:') end
if (ai.aspects.caution <= 0) then
if (ai.aspects.retreat_factor <= 0) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end

View file

@ -7,40 +7,45 @@ 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 function print_dbg(...)
local show_debug_info = false -- manually set to true/false depending on whether output is desired
if wesnoth.game_config.debug and show_debug_info then
std_print('Retreat debug: ', ...)
end
end
local retreat_functions = {}
function retreat_functions.min_hp(unit)
-- The minimum hp to retreat is a function of level and terrain defense
-- We want to stay longer on good terrain and leave early on very bad terrain
-- The minimum hp to retreat is a function of hitpoints and terrain defense
-- We want to stay longer on good terrain and leave early on bad terrain
-- It can be influenced by the 'retreat_factor' AI aspect
-- Take caution into account here. We want the multiplier to be:
-- 1 for default caution (0.25)
-- 0 for minimal caution <= 0
-- 2 for caution = 1
local caution_factor = ai.aspects.caution
if (caution_factor < 0) then caution_factor = 0 end
caution_factor = math.sqrt(caution_factor) * 2
local retreat_factor = ai.aspects.retreat_factor
local hp_per_level = (100 - unit:defense_on(wesnoth.get_terrain(unit.x, unit.y)))/15 * caution_factor
local level = unit.level
-- Leaders are more valuable and should retreat earlier
if unit.canrecruit then retreat_factor = retreat_factor * 1.5 end
-- Leaders are considered to be higher level because of their value
if unit.canrecruit then level = level+2 end
-- Higher retreat willingness on bad terrain
local retreat_factor = retreat_factor * (100 - unit:defense_on(wesnoth.get_terrain(unit.x, unit.y))) / 50
local min_hp = hp_per_level*(level+2)
local min_hp = retreat_factor * unit.max_hitpoints
-- Account for poison damage on next turn
if unit.status.poisoned then min_hp = min_hp + wesnoth.game_config.poison_amount end
-- Make sure that units are actually injured (only relevant for low-HP units)
-- Want this to be roughly half the units HP at caution=0, close to full HP at caution=1
local hp_factor = 0.5 + 0.25 * caution_factor
if (hp_factor > 1) then hp_factor = 1 end
local max_min_hp = (unit.max_hitpoints - 4) * hp_factor
-- Large values of retreat_factor could cause fully healthy units to retreat.
-- We require a unit to be down more than 10 HP, or half its HP for units with less than 20 max_HP.
local max_hp = unit.max_hitpoints
local max_min_hp = math.max(max_hp - 10, max_hp / 2)
if (min_hp > max_min_hp) then
min_hp = max_min_hp
end
local retreat_str = ''
if (unit.hitpoints < min_hp) then retreat_str = ' --> retreat' end
print_dbg(string.format('%-20s %3d/%-3d HP threshold: %5.1f HP%s', unit.id, unit.hitpoints, unit.max_hitpoints, min_hp, retreat_str))
return min_hp
end
@ -99,11 +104,7 @@ function retreat_functions.retreat_injured_units(units, avoid_map)
end
end
function retreat_functions.get_healing_locations()
local possible_healers = AH.get_live_units {
{ "filter_side", {{"allied_with", {side = wesnoth.current.side} }} }
}
function retreat_functions.get_healing_locations(possible_healers)
local healing_locs = LS.create()
for i,u in ipairs(possible_healers) do
-- Only consider healers that cannot move this turn
@ -124,7 +125,7 @@ function retreat_functions.get_healing_locations()
local old_values = healing_locs:get(x, y) or {0, 0}
local best_heal = math.max(old_values[0] or heal_amount)
local best_cure = math.max(old_values[1] or cure)
healing_locs:insert(u.x, u.y, {best_heal, best_cure})
healing_locs:insert(x, y, {best_heal, best_cure})
end
end
end
@ -134,11 +135,20 @@ function retreat_functions.get_healing_locations()
end
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)
local retreat_enemy_weight = ai.aspects.retreat_enemy_weight
local healing_locs = retreat_functions.get_healing_locations()
local allies = AH.get_live_units {
{ "filter_side", { {"allied_with", { side = wesnoth.current.side } } } }
}
local healing_locs = retreat_functions.get_healing_locations(allies)
-- These operations are somewhat expensive, don't do them if not necessary
local enemy_attack_map, ally_attack_map
if (retreat_enemy_weight ~= 0) then
local enemies = AH.get_attackable_enemies()
enemy_attack_map = BC.get_attack_map(enemies)
ally_attack_map = BC.get_attack_map(allies)
end
local max_rating, best_loc, best_unit = - math.huge
for i,u in ipairs(healees) do
@ -183,12 +193,13 @@ function retreat_functions.get_retreat_injured_units(healees, regen_amounts, avo
if u.status.slowed then base_rating = base_rating + 4 end
base_rating = base_rating * 1000
print_dbg(string.format('check retreat hexes for: %-20s base_rating = %f8.1', u.id, base_rating))
for j,loc in ipairs(possible_locations) do
local unit_in_way = wesnoth.units.get(loc[1], loc[2])
if (not AH.is_visible_unit(wesnoth.current.side, unit_in_way))
or ((unit_in_way.moves > 0) and (unit_in_way.side == wesnoth.current.side))
then
local rating = base_rating
local heal_score = 0
if regen_amounts[i] then
heal_score = math.min(regen_amounts[i], u.max_hitpoints - u.hitpoints)
@ -206,35 +217,58 @@ function retreat_functions.get_retreat_injured_units(healees, regen_amounts, avo
end
end
-- Huge penalty for each enemy that can reach location,
-- this is the single most important point (and non-linear)
local enemy_count = enemy_attack_map.units:get(loc[1], loc[2]) or 0
rating = rating - enemy_count * 100000
-- Figure out the enemy threat - this is also needed to assess whether rest healing is likely
local enemy_rating, enemy_count = 0, 0
if (retreat_enemy_weight ~= 0) then
enemy_count = enemy_attack_map.units:get(loc[1], loc[2]) or 0
local enemy_hp = enemy_attack_map.hitpoints:get(loc[1], loc[2]) or 0
local ally_hp = ally_attack_map.hitpoints:get(loc[1], loc[2]) or 0
local hp_diff = ally_hp - enemy_hp * math.abs(retreat_enemy_weight)
if (hp_diff > 0) then hp_diff = 0 end
-- Penalty based on terrain defense for unit
rating = rating - (100 - u:defense_on(wesnoth.get_terrain(loc[1], loc[2])))/10
-- The rating is mostly the HP difference, but we still want to
-- avoid threatened hexes even if we have the advantage
enemy_rating = hp_diff - enemy_count * math.abs(retreat_enemy_weight)
end
if (loc[1] == u.x) and (loc[2] == u.y) and (not u.status.poisoned) then
if is_healthy or enemy_count == 0 then
-- Bonus if we can rest heal
heal_score = heal_score + wesnoth.game_config.rest_heal_amount
end
elseif unit_in_way then
-- Penalty if a unit has to move out of the way
-- (based on hp of moving unit)
rating = rating + unit_in_way.hitpoints - unit_in_way.max_hitpoints
end
rating = rating + heal_score^2
-- Only consider healing locations, except when retreat_enemy_weight is negative
if (heal_score > 0) or (retreat_enemy_weight < 0) then
local rating = base_rating + heal_score^2
rating = rating + enemy_rating
if (rating > max_rating) then
max_rating, best_loc, best_unit = rating, loc, u
-- Penalty based on terrain defense for unit
rating = rating - (100 - u:defense_on(wesnoth.get_terrain(loc[1], loc[2])))/10
-- Penalty if a unit has to move out of the way
-- (based on hp of moving unit)
if unit_in_way and ((loc[1] ~= u.x) or (loc[2] ~= u.y)) then
rating = rating + unit_in_way.hitpoints - unit_in_way.max_hitpoints
end
print_dbg(string.format(' possible retreat hex: %3d,%-3d rating = %9.1f (heal_score = %5.1f, enemy_rating = %9.1f)', loc[1], loc[2], rating, heal_score, enemy_rating))
if (rating > max_rating) then
max_rating, best_loc, best_unit = rating, loc, u
end
end
end
end
end
return best_unit, best_loc, enemy_attack_map.units:get(best_loc[1], best_loc[2]) or 0
local threat = 0
if best_unit then
threat = enemy_attack_map and enemy_attack_map.units:get(best_loc[1], best_loc[2]) or 0
print_dbg(string.format('found unit to retreat: %s --> %d,%d', best_unit.id, best_loc[1], best_loc[2]))
end
return best_unit, best_loc, threat
end
return retreat_functions

View file

@ -92,6 +92,8 @@
[/value]
[/default]
[/aspect]
{DEFAULT_ASPECT_VALUE retreat_enemy_weight 1.0}
{DEFAULT_ASPECT_VALUE retreat_factor 0.25}
{DEFAULT_ASPECT_VALUE scout_village_targeting 3}
{DEFAULT_ASPECT_VALUE simple_targeting no}
{DEFAULT_ASPECT_VALUE support_villages no}

View file

@ -60,7 +60,7 @@
[switch]
key=id
[case]
value=aggression,caution,leader_aggression,leader_value,scout_village_targeting,village_value,recruitment_diversity
value=aggression,caution,leader_aggression,leader_value,retreat_enemy_weight,retreat_factor,scout_village_targeting,village_value,recruitment_diversity
super="{BASE}~real"
[/case]
[case]

View file

@ -48,6 +48,8 @@
{AI_ASPECT_KEY passive_leader_shares_keep s_bool}
{AI_ASPECT_KEY recruitment_diversity s_real}
{AI_ASPECT_KEY recruitment_randomness s_int}
{AI_ASPECT_KEY retreat_enemy_weight s_real}
{AI_ASPECT_KEY retreat_factor s_real}
{AI_ASPECT_KEY scout_village_targeting s_real}
{AI_ASPECT_KEY simple_targeting s_bool}
{AI_ASPECT_KEY support_villages s_bool}

View file

@ -154,6 +154,8 @@
{AI_MODIFY_MATCH_ASPECT caution real}
{AI_MODIFY_MATCH_ASPECT leader_aggression real}
{AI_MODIFY_MATCH_ASPECT leader_value real}
{AI_MODIFY_MATCH_ASPECT retreat_enemy_weight real}
{AI_MODIFY_MATCH_ASPECT retreat_factor real}
{AI_MODIFY_MATCH_ASPECT scout_village_targeting real}
{AI_MODIFY_MATCH_ASPECT village_value real}
{AI_MODIFY_MATCH_ASPECT recruitment_diversity real}
@ -175,4 +177,3 @@
{AI_MODIFY_MATCH_ASPECT attacks attacks}
[/if]
[/tag]

View file

@ -76,7 +76,7 @@
("rulebase"))
("ai"
("team_formula" "register_candidate_action" "candidate_action" "not" "protect_area" "aspect" "leader_goal" "avoid" "vars" "modify_ai" "engine" "goal" "stage")
("priority" "formula" "eval_list" "_stage" "_aspect" "leader" "loop_formula" "y" "x" "passive_leader_shares_keep" "leader_aggression" "number_of_possible_recruits_to_force_recruit" "time_of_day" "ai_algorithm" "scout_village_targeting" "turns" "attack_depth" "grouping" "villages_per_scout" "leader_value" "village_value" "leader_shares_keep" "passive_leader" "recruitment_pattern" "simple_targeting" "recruitment_ignore_bad_combat" "recruitment_ignore_bad_movement" "caution" "aggression" "version" "description" "id"))
("priority" "formula" "eval_list" "_stage" "_aspect" "leader" "loop_formula" "y" "x" "passive_leader_shares_keep" "leader_aggression" "number_of_possible_recruits_to_force_recruit" "time_of_day" "ai_algorithm" "retreat_enemy_weight" "retreat_factor" "scout_village_targeting" "turns" "attack_depth" "grouping" "villages_per_scout" "leader_value" "village_value" "leader_shares_keep" "passive_leader" "recruitment_pattern" "simple_targeting" "recruitment_ignore_bad_combat" "recruitment_ignore_bad_movement" "caution" "aggression" "version" "description" "id"))
("side"
("leader" "variables" "goal" "village" "unit" "ai" "modifications")
("recall_cost" "village_support" "gold_lock" "faction_from_recruit" "income_lock" "team_lock" "disallow_observers" "variation" "ai_special" "hitpoints" "overlays" "ai_algorithm" "generate_name" "random_traits" "extra_recruit" "y" "x" "allow_player" "faction" "image" "experience" "race" "facing" "moves" "color" "share_maps" "share_view" "shroud" "fog" "profile" "scroll_to_leader" "village_gold" "unrenamable" "gender" "save_id" "income" "no_leader" "gold" "recruit" "canrecruit" "user_team_name" "team_name" "persistent" "max_moves" "type" "id" "name" "hidden" "controller" "side"))

View file

@ -196,6 +196,8 @@ readonly_context_impl::readonly_context_impl(side_context &context, const config
recruitment_randomness_(),
recruitment_save_gold_(),
recursion_counter_(context.get_recursion_count()),
retreat_enemy_weight_(),
retreat_factor_(),
scout_village_targeting_(),
simple_targeting_(),
srcdst_(),
@ -225,6 +227,8 @@ readonly_context_impl::readonly_context_impl(side_context &context, const config
add_known_aspect("recruitment_pattern",recruitment_pattern_);
add_known_aspect("recruitment_randomness",recruitment_randomness_);
add_known_aspect("recruitment_save_gold",recruitment_save_gold_);
add_known_aspect("retreat_enemy_weight",retreat_enemy_weight_);
add_known_aspect("retreat_factor",retreat_factor_);
add_known_aspect("scout_village_targeting",scout_village_targeting_);
add_known_aspect("simple_targeting",simple_targeting_);
add_known_aspect("support_villages",support_villages_);
@ -754,6 +758,22 @@ const config readonly_context_impl::get_recruitment_save_gold() const
return config();
}
double readonly_context_impl::get_retreat_enemy_weight() const
{
if (retreat_enemy_weight_) {
return retreat_enemy_weight_->get();
}
return 1;
}
double readonly_context_impl::get_retreat_factor() const
{
if (retreat_factor_) {
return retreat_factor_->get();
}
return 1;
}
double readonly_context_impl::get_scout_village_targeting() const
{
if (scout_village_targeting_) {

View file

@ -264,6 +264,10 @@ public:
virtual const config get_recruitment_save_gold() const = 0;
virtual double get_retreat_enemy_weight() const = 0;
virtual double get_retreat_factor() const = 0;
virtual double get_scout_village_targeting() const = 0;
virtual bool get_simple_targeting() const = 0;
@ -713,6 +717,16 @@ public:
return target_->get_srcdst();
}
virtual double get_retreat_enemy_weight() const override
{
return target_->get_retreat_enemy_weight();
}
virtual double get_retreat_factor() const override
{
return target_->get_retreat_factor();
}
virtual double get_scout_village_targeting() const override
{
return target_->get_scout_village_targeting();
@ -1180,6 +1194,10 @@ public:
virtual const config get_recruitment_save_gold() const override;
virtual double get_retreat_enemy_weight() const override;
virtual double get_retreat_factor() const override;
virtual double get_scout_village_targeting() const override;
virtual bool get_simple_targeting() const override;
@ -1295,6 +1313,8 @@ private:
typesafe_aspect_ptr<int> recruitment_randomness_;
typesafe_aspect_ptr<config> recruitment_save_gold_;
recursion_counter recursion_counter_;
typesafe_aspect_ptr<double> retreat_enemy_weight_;
typesafe_aspect_ptr<double> retreat_factor_;
typesafe_aspect_ptr<double> scout_village_targeting_;
typesafe_aspect_ptr<bool> simple_targeting_;
mutable move_map srcdst_;

View file

@ -346,6 +346,14 @@ variant formula_ai::get_value(const std::string& key) const
}
return variant(vars);
} else if(key == "retreat_enemy_weight")
{
return variant(get_retreat_enemy_weight()*1000,variant::DECIMAL_VARIANT);
} else if(key == "retreat_factor")
{
return variant(get_retreat_factor()*1000,variant::DECIMAL_VARIANT);
} else if(key == "scout_village_targeting")
{
return variant(get_scout_village_targeting()*1000,variant::DECIMAL_VARIANT);

View file

@ -498,6 +498,22 @@ static int cfun_ai_get_recruitment_pattern(lua_State *L)
return 1;
}
static int cfun_ai_get_retreat_enemy_weight(lua_State *L)
{
DEPRECATED_ASPECT_MESSAGE("retreat_enemy_weight");
double retreat_enemy_weight = get_readonly_context(L).get_retreat_enemy_weight();
lua_pushnumber(L, retreat_enemy_weight);
return 1;
}
static int cfun_ai_get_retreat_factor(lua_State *L)
{
DEPRECATED_ASPECT_MESSAGE("retreat_factor");
double retreat_factor = get_readonly_context(L).get_retreat_factor();
lua_pushnumber(L, retreat_factor);
return 1;
}
static int cfun_ai_get_scout_village_targeting(lua_State *L)
{
DEPRECATED_ASPECT_MESSAGE("scout_village_targeting");
@ -896,6 +912,8 @@ static int impl_ai_get(lua_State* L)
{ "get_passive_leader", &cfun_ai_get_passive_leader },
{ "get_passive_leader_shares_keep", &cfun_ai_get_passive_leader_shares_keep },
{ "get_recruitment_pattern", &cfun_ai_get_recruitment_pattern },
{ "get_retreat_enemy_weight", &cfun_ai_get_retreat_enemy_weight },
{ "get_retreat_factor", &cfun_ai_get_retreat_factor },
{ "get_scout_village_targeting", &cfun_ai_get_scout_village_targeting },
{ "get_simple_targeting", &cfun_ai_get_simple_targeting },
{ "get_support_villages", &cfun_ai_get_support_villages },

View file

@ -254,6 +254,8 @@ const std::string holder::get_ai_overview()
s << "recruitment_randomness: " << this->ai_->get_recruitment_randomness() << std::endl;
s << "recruitment_save_gold: " << std::endl << "----config begin----" << std::endl;
s << this->ai_->get_recruitment_save_gold() << "-----config end-----" << std::endl;
s << "retreat_enemy_weight: " << this->ai_->get_retreat_enemy_weight() << std::endl;
s << "retreat_factor: " << this->ai_->get_retreat_factor() << std::endl;
s << "scout_village_targeting: " << this->ai_->get_scout_village_targeting() << std::endl;
s << "simple_targeting: " << cfg["simple_targeting"] << std::endl;
s << "support_villages: " << cfg["support_villages"] << std::endl;

View file

@ -236,6 +236,12 @@ static register_aspect_factory< composite_aspect<int>>
static register_aspect_factory< composite_aspect<config>>
recruitment_save_gold__composite_aspect_factory("recruitment_save_gold*composite_aspect");
static register_aspect_factory< composite_aspect<double>>
retreat_enemy_weight__composite_aspect_factory("retreat_enemy_weight*composite_aspect");
static register_aspect_factory< composite_aspect<double>>
retreat_factor__composite_aspect_factory("retreat_factor*composite_aspect");
static register_aspect_factory< composite_aspect<double>>
scout_village_targeting__composite_aspect_factory("scout_village_targeting*composite_aspect");
@ -307,6 +313,12 @@ static register_aspect_factory< standard_aspect<int>>
static register_aspect_factory< standard_aspect<config>>
recruitment_save_gold__standard_aspect_factory("recruitment_save_gold*standard_aspect");
static register_aspect_factory< standard_aspect<double>>
retreat_enemy_weight__standard_aspect_factory("retreat_enemy_weight*standard_aspect");
static register_aspect_factory< standard_aspect<double>>
retreat_factor__standard_aspect_factory("retreat_factor*standard_aspect");
static register_aspect_factory< standard_aspect<double>>
scout_village_targeting__standard_aspect_factory("scout_village_targeting*standard_aspect");
@ -382,6 +394,12 @@ static register_aspect_factory< standard_aspect<int>>
static register_aspect_factory< standard_aspect<config>>
recruitment_save_gold__standard_aspect_factory2("recruitment_save_gold*");
static register_aspect_factory< standard_aspect<double>>
retreat_enemy_weight__standard_aspect_factory2("retreat_enemy_weight*");
static register_aspect_factory< standard_aspect<double>>
retreat_factor__standard_aspect_factory2("retreat_factor*");
static register_aspect_factory< standard_aspect<double>>
scout_village_targeting__standard_aspect_factory2("scout_village_targeting*");
@ -435,6 +453,12 @@ static register_lua_aspect_factory< lua_aspect<utils::variant<bool, std::vector<
static register_lua_aspect_factory< lua_aspect<utils::variant<bool, std::vector<std::string>>>>
passive_leader_shares_keep__lua_aspect_factory("passive_leader_shares_keep*lua_aspect");
static register_lua_aspect_factory< lua_aspect<double>>
retreat_enemy_weight__lua_aspect_factory("retreat_enemy_weight*lua_aspect");
static register_lua_aspect_factory< lua_aspect<double>>
retreat_factor__lua_aspect_factory("retreat_factor*lua_aspect");
static register_lua_aspect_factory< lua_aspect<double>>
scout_village_targeting__lua_aspect_factory("scout_village_targeting*lua_aspect");