Lua AIs: do not use hard-coded values for modifiable parameters

This commit is contained in:
mattsc 2018-11-05 20:22:11 -08:00
parent e3b2cbfc25
commit 0c57ae16e4
7 changed files with 77 additions and 61 deletions

View file

@ -800,26 +800,23 @@ function battle_calcs.attack_rating(attacker, defender, dst, cfg, cache)
-- Average damage to unit is negative rating
local damage = attacker.hitpoints - att_stats.average_hp
-- Count poisoned as additional 8 HP damage times probability of being poisoned
-- Count poisoned as additional damage done by poison times probability of being poisoned
if (att_stats.poisoned ~= 0) then
damage = damage + 8 * (att_stats.poisoned - att_stats.hp_chance[0])
damage = damage + wesnoth.game_config.poison_amount * (att_stats.poisoned - att_stats.hp_chance[0])
end
-- Count slowed as additional 6 HP damage times probability of being slowed
if (att_stats.slowed ~= 0) then
damage = damage + 6 * (att_stats.slowed - att_stats.hp_chance[0])
end
-- If attack is from a village, we count that as a 10 HP bonus
local is_village = wesnoth.get_terrain_info(wesnoth.get_terrain(dst[1], dst[2])).village
if is_village then
damage = damage - 10.
end
-- If attack is from a village (or other healing location), count that as slightly more than the healing amount
damage = damage - 1.25 * wesnoth.get_terrain_info(wesnoth.get_terrain(dst[1], dst[2])).healing
-- If attack is adjacent to an unoccupied village, that's bad
-- Equivalently, if attack is adjacent to an unoccupied village, that's bad
for xa,ya in H.adjacent_tiles(dst[1], dst[2]) do
local is_adjacent_village = wesnoth.get_terrain_info(wesnoth.get_terrain(xa, ya)).village
if is_adjacent_village and (not wesnoth.get_unit(xa, ya)) then
damage = damage + 10
local healing = wesnoth.get_terrain_info(wesnoth.get_terrain(xa, ya)).healing
if (healing > 0) and (not wesnoth.get_unit(xa, ya)) then
damage = damage + 1.25 * healing
end
end
@ -835,10 +832,10 @@ function battle_calcs.attack_rating(attacker, defender, dst, cfg, cache)
-- proportional to the chance of it happening and the chance of not dying itself
local level_bonus = 0.
local defender_level = defender.level
if (attacker.max_experience - attacker.experience <= defender_level) then
if (attacker.max_experience - attacker.experience <= defender_level * wesnoth.game_config.combat_experience) then
level_bonus = 1. - att_stats.hp_chance[0]
else
if (attacker.max_experience - attacker.experience <= defender_level * 8) then
if (attacker.max_experience - attacker.experience <= defender_level * wesnoth.game_config.kill_experience) then
level_bonus = (1. - att_stats.hp_chance[0]) * def_stats.hp_chance[0]
end
end
@ -857,20 +854,17 @@ function battle_calcs.attack_rating(attacker, defender, dst, cfg, cache)
------ Now (most of) the same for the defender ------
-- Average damage to defender is positive rating
local damage = defender.hitpoints - def_stats.average_hp
-- Count poisoned as additional 8 HP damage times probability of being poisoned
-- Count poisoned as additional damage done by poison times probability of being poisoned
if (def_stats.poisoned ~= 0) then
damage = damage + 8 * (def_stats.poisoned - def_stats.hp_chance[0])
damage = damage + wesnoth.game_config.poison_amount * (def_stats.poisoned - def_stats.hp_chance[0])
end
-- Count slowed as additional 6 HP damage times probability of being slowed
if (def_stats.slowed ~= 0) then
damage = damage + 6 * (def_stats.slowed - def_stats.hp_chance[0])
end
-- If defender is on a village, we count that as a 10 HP bonus
local is_village = wesnoth.get_terrain_info(wesnoth.get_terrain(defender.x, defender.y)).village
if is_village then
damage = damage - 10.
end
-- If defender is on a village (or other healing location), count that as slightly more than the healing amount
damage = damage - 1.25 * wesnoth.get_terrain_info(wesnoth.get_terrain(defender.x, defender.y)).healing
if (damage < 0) then damage = 0. end
@ -884,10 +878,10 @@ function battle_calcs.attack_rating(attacker, defender, dst, cfg, cache)
-- proportional to the chance of it happening and the chance of not dying itself
local defender_level_penalty = 0.
local attacker_level = attacker.level
if (defender.max_experience - defender.experience <= attacker_level) then
if (defender.max_experience - defender.experience <= attacker_level * wesnoth.game_config.combat_experience) then
defender_level_penalty = 1. - def_stats.hp_chance[0]
else
if (defender.max_experience - defender.experience <= attacker_level * 8) then
if (defender.max_experience - defender.experience <= attacker_level * wesnoth.game_config.kill_experience) then
defender_level_penalty = (1. - def_stats.hp_chance[0]) * att_stats.hp_chance[0]
end
end

View file

@ -56,12 +56,12 @@ function ca_attack_highxp:evaluation(cfg, data)
for i_t,enemy in ipairs(attacks_aspect.enemy) do
if AH.is_attackable_enemy(enemy) then
local XP_to_levelup = enemy.max_experience - enemy.experience
if (max_unit_level >= XP_to_levelup) then
if (max_unit_level * wesnoth.game_config.combat_experience >= XP_to_levelup) then
local potential_target = false
local ind_attackers, ind_other_units = {}, {}
for i_u,unit in ipairs(units) do
if (M.distance_between(enemy.x, enemy.y, unit.x, unit.y) <= unit.moves + 1) then
if (unit.level >= XP_to_levelup) then
if (unit.level * wesnoth.game_config.combat_experience >= XP_to_levelup) then
potential_target = true
table.insert(ind_attackers, i_u)
else

View file

@ -47,7 +47,7 @@ function ca_spread_poison:evaluation(cfg, data)
local on_village = wesnoth.get_terrain_info(defender_terrain).village
-- Also, poisoning units that would level up through the attack or could level on their turn as a result is very bad
local about_to_level = defender.max_experience - defender.experience <= (attacker.level * 2)
local about_to_level = defender.max_experience - defender.experience <= (attacker.level * 2 * wesnoth.game_config.combat_experience)
if (not cant_poison) and (not on_village) and (not about_to_level) then
-- Strongest enemy gets poisoned first

View file

@ -223,7 +223,7 @@ return {
local poison_damage = 0
if poison then
-- Add poison damage * probability of poisoning
poison_damage = 8*(1-((1-defense)^attack.number))
poison_damage = wesnoth.game_config.poison_amount*(1-((1-defense)^attack.number))
end
if (not best_attack) or (attack_damage+poison_damage > best_damage+best_poison_damage) then

View file

@ -21,7 +21,7 @@ function retreat_functions.min_hp(unit)
local min_hp = hp_per_level*(level+2)
-- Account for poison damage on next turn
if unit.status.poisoned then min_hp = min_hp + 8 end
if unit.status.poisoned then min_hp = min_hp + wesnoth.game_config.poison_amount end
-- Make sure that units are actually injured
if min_hp > unit.max_hitpoints - 4 then
@ -35,11 +35,22 @@ end
-- Return nil if no unit needs to retreat
function retreat_functions.retreat_injured_units(units)
-- Split units into those that regenerate and those that do not
local regen, non_regen = {}, {}
local regen, regen_amounts, non_regen = {}, {}, {}
for i,u in ipairs(units) do
if u.hitpoints < retreat_functions.min_hp(u) then
if u:ability('regenerate') then
-- Find the best regeneration ability and use it to estimate hp regained by regeneration
local abilities = wml.get_child(u.__cfg, "abilities")
local regen_amount = 0
if abilities then
for regen in wml.child_range(abilities, "regenerate") do
if regen.value > regen_amount then
regen_amount = regen.value
end
end
end
table.insert(regen, u)
table.insert(regen_amounts, regen_amount)
else
table.insert(non_regen, u)
end
@ -49,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, false)
unit_nr, loc_nr, threat_nr = retreat_functions.get_retreat_injured_units(non_regen, {})
if unit_nr and (threat_nr == 0) then
return unit_nr, loc_nr, threat_nr
end
@ -58,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, true)
unit_r, loc_r, threat_r = retreat_functions.get_retreat_injured_units(regen, regen_amounts)
if unit_r and (threat_r == 0) then
return unit_r, loc_r, threat_r
end
@ -107,7 +118,7 @@ function retreat_functions.get_healing_locations()
return healing_locs
end
function retreat_functions.get_retreat_injured_units(healees, regenerates)
function retreat_functions.get_retreat_injured_units(healees, regen_amounts)
-- Only retreat to safe locations
local enemies = AH.get_attackable_enemies()
local enemy_attack_map = BC.get_attack_map(enemies)
@ -119,7 +130,7 @@ function retreat_functions.get_retreat_injured_units(healees, regenerates)
local possible_locations = wesnoth.find_reach(u)
-- TODO: avoid ally's villages (may be preferable to lower rating so they will
-- be used if unit is very injured)
if not regenerates then
if (not regen_amounts[i]) then
-- Unit cannot self heal, make the terrain do it for us if possible
local location_subset = {}
for j,loc in ipairs(possible_locations) do
@ -151,7 +162,7 @@ function retreat_functions.get_retreat_injured_units(healees, regenerates)
end
local base_rating = - u.hitpoints + u.max_hitpoints / 2.
if u.status.poisoned then base_rating = base_rating + 8 end
if u.status.poisoned then base_rating = base_rating + wesnoth.game_config.poison_amount end
if u.status.slowed then base_rating = base_rating + 4 end
base_rating = base_rating * 1000
@ -162,12 +173,12 @@ function retreat_functions.get_retreat_injured_units(healees, regenerates)
then
local rating = base_rating
local heal_score = 0
if regenerates then
heal_score = math.min(8, u.max_hitpoints - u.hitpoints)
if regen_amounts[i] then
heal_score = math.min(regen_amounts[i], u.max_hitpoints - u.hitpoints)
else
if u.status.poisoned then
if loc[4] > 0 then
heal_score = math.min(8, u.hitpoints - 1)
heal_score = math.min(wesnoth.game_config.poison_amount, u.hitpoints - 1)
if loc[4] == 2 then
-- This value is arbitrary, it just represents the ability to heal on the turn after
heal_score = heal_score + 1
@ -189,7 +200,7 @@ function retreat_functions.get_retreat_injured_units(healees, regenerates)
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 + 2
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

View file

@ -380,24 +380,24 @@ function ca_bottleneck_move:evaluation(cfg, data)
for _,attack in ipairs(attacks) do
-- Only do calc. if there's a theoretical chance for leveling up (speeds things up a lot)
if (attack.x == loc[1]) and (attack.y == loc[2]) and
(unit.max_experience - unit.experience <= 8 * attack.defender_level)
(unit.max_experience - unit.experience <= wesnoth.game_config.kill_experience * attack.defender_level)
then
for n_weapon,weapon in ipairs(unit.attacks) do
local att_stats, def_stats = BC.simulate_combat_loc(unit, { attack.x, attack.y }, attack.defender, n_weapon)
-- Execute level-up attack when:
-- 1. max_experience-experience <= target.level and chance to die = 0
-- 2. max_experience-experience <= target.level*8 and chance to die = 0
-- 1. max_experience-experience <= target.level*combat_experience and chance to die = 0
-- 2. max_experience-experience <= target.level*kill_experience and chance to die = 0
-- and chance to kill > 66% and remaining av hitpoints > 20
-- #1 is a definite level up, #2 is not, so #1 gets priority
local level_up_rating = 0
if (unit.max_experience - unit.experience <= attack.defender_level) then
if (unit.max_experience - unit.experience <= wesnoth.game_config.combat_experience * attack.defender_level) then
if (att_stats.hp_chance[0] == 0) then
-- Weakest enemy is best (favors stronger weapon)
level_up_rating = 15000 - def_stats.average_hp
end
else
if (unit.max_experience - unit.experience <= 8 * attack.defender_level)
if (unit.max_experience - unit.experience <= wesnoth.game_config.kill_experience * attack.defender_level)
and (att_stats.hp_chance[0] == 0)
and (def_stats.hp_chance[0] >= 0.66)
and (att_stats.average_hp >= 20)

View file

@ -49,7 +49,10 @@ function ca_fast_attack_utils.gamedata_setup()
local village_map = {}
for _,village in ipairs(wesnoth.get_villages()) do
if (not village_map[village[1]]) then village_map[village[1]] = {} end
village_map[village[1]][village[2]] = { owner = wesnoth.get_village_owner(village[1], village[2]) }
village_map[village[1]][village[2]] = {
owner = wesnoth.get_village_owner(village[1], village[2]),
healing = wesnoth.get_terrain_info(wesnoth.get_terrain(village[1], village[2])).healing
}
end
gamedata.village_map = village_map
@ -100,7 +103,11 @@ function ca_fast_attack_utils.single_unit_info(unit_proxy)
local abilities = wml.get_child(unit_proxy.__cfg, "abilities")
if abilities then
for _,ability in ipairs(abilities) do
single_unit_info[ability[1]] = true
if ability[1] == 'regenerate' then
single_unit_info[ability[1]] = ability[2].value
else
single_unit_info[ability[1]] = true
end
end
end
@ -174,7 +181,7 @@ function ca_fast_attack_utils.is_acceptable_attack(damage_taken, damage_done, ag
return (damage_done / damage_taken) >= (1 - aggression)
end
function ca_fast_attack_utils.damage_rating_unit(attacker_info, defender_info, att_stat, def_stat, is_village, cfg)
function ca_fast_attack_utils.damage_rating_unit(attacker_info, defender_info, att_stat, def_stat, healing, cfg)
-- Calculate the rating for the damage received by a single attacker on a defender.
-- The attack att_stat both for the attacker and the defender need to be precalculated for this.
-- Unit information is passed in unit_infos format, rather than as unit proxy tables for speed reasons.
@ -184,7 +191,7 @@ function ca_fast_attack_utils.damage_rating_unit(attacker_info, defender_info, a
-- Input parameters:
-- @attacker_info, @defender_info: unit_info tables produced by ca_fast_attack_utils.get_unit_info()
-- @att_stat, @def_stat: attack statistics for the two units
-- @is_village: whether the hex from which the attacker attacks is a village
-- @healing: the healing given by a village
-- Set to nil or false if not, to anything if it is a village (does not have to be a boolean)
--
-- Optional parameters:
@ -194,21 +201,21 @@ function ca_fast_attack_utils.damage_rating_unit(attacker_info, defender_info, a
local damage = attacker_info.hitpoints - att_stat.average_hp
-- Count poisoned as additional 8 HP damage times probability of being poisoned
-- Count poisoned as additional damage done by poison times probability of being poisoned
if (att_stat.poisoned ~= 0) then
damage = damage + 8 * (att_stat.poisoned - att_stat.hp_chance[0])
damage = damage + wesnoth.game_config.poison_amount * (att_stat.poisoned - att_stat.hp_chance[0])
end
-- Count slowed as additional 4 HP damage times probability of being slowed
if (att_stat.slowed ~= 0) then
damage = damage + 4 * (att_stat.slowed - att_stat.hp_chance[0])
end
-- If attack is from a village, we count that as an 8 HP bonus
if is_village then
damage = damage - 8.
-- Otherwise only: if attacker can regenerate, this is an 8 HP bonus
elseif attacker_info.regenerate then
damage = damage - 8.
-- If attack is from a village, count its healing_value
if healing then
damage = damage - healing
-- Otherwise only: if attacker can regenerate, add the regeneration bonus
else
damage = damage - (attacker_info.regenerate or 0)
end
if (damage < 0) then damage = 0 end
@ -227,9 +234,9 @@ function ca_fast_attack_utils.damage_rating_unit(attacker_info, defender_info, a
if (defender_level == 0) then defender_level = 0.5 end -- L0 units
local level_bonus = 0.
if (attacker_info.max_experience - attacker_info.experience <= defender_level) then
if (attacker_info.max_experience - attacker_info.experience <= defender_level * wesnoth.game_config.combat_experience) then
level_bonus = 1. - att_stat.hp_chance[0]
elseif (attacker_info.max_experience - attacker_info.experience <= defender_level * 8) then
elseif (attacker_info.max_experience - attacker_info.experience <= defender_level * wesnoth.game_config.kill_experience) then
level_bonus = (1. - att_stat.hp_chance[0]) * def_stat.hp_chance[0]
end
@ -283,18 +290,22 @@ function ca_fast_attack_utils.attack_rating(attacker_infos, defender_info, dsts,
local attacker_rating = 0
for i,attacker_info in ipairs(attacker_infos) do
local attacker_on_village = gamedata.village_map[dsts[i][1]] and gamedata.village_map[dsts[i][1]][dsts[i][2]]
local attacker_healing = gamedata.village_map[dsts[i][1]]
and gamedata.village_map[dsts[i][1]][dsts[i][2]]
and gamedata.village_map[dsts[i][1]][dsts[i][2]].healing
attacker_rating = attacker_rating + ca_fast_attack_utils.damage_rating_unit(
attacker_info, defender_info, att_stats[i], def_stat, attacker_on_village, cfg
attacker_info, defender_info, att_stats[i], def_stat, attacker_healing, cfg
)
end
-- attacker_info is passed only to figure out whether the attacker might level up
-- TODO: This is only works for the first attacker in a combo at the moment
local defender_x, defender_y = defender_info.x, defender_info.y
local defender_on_village = gamedata.village_map[defender_x] and gamedata.village_map[defender_x][defender_y]
local defender_healing = gamedata.village_map[defender_x]
and gamedata.village_map[defender_x][defender_y]
and gamedata.village_map[defender_x][defender_y].healing
local defender_rating = ca_fast_attack_utils.damage_rating_unit(
defender_info, attacker_infos[1], def_stat, att_stats[1], defender_on_village, cfg
defender_info, attacker_infos[1], def_stat, att_stats[1], defender_healing, cfg
)
-- Now we add some extra ratings. They are positive for attacks that should be preferred
@ -309,7 +320,7 @@ function ca_fast_attack_utils.attack_rating(attacker_infos, defender_info, dsts,
-- If defender is on a village, add a bonus rating (we want to get rid of those preferentially)
-- This is in addition to the damage bonus already included above (but as an extra rating)
if defender_on_village then
if defender_healing then
extra_rating = extra_rating + 10.
end