mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-26 15:33:16 +00:00

Insert these calls in loops that do expensive calculations for the CAs of the default and experimental AIs.
306 lines
14 KiB
Lua
306 lines
14 KiB
Lua
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
|
local LS = wesnoth.require "location_set"
|
|
local M = wesnoth.map
|
|
|
|
-- Evaluation process:
|
|
--
|
|
-- Find all enemy units that could be caused to level up by an attack
|
|
-- - If only units that would cause them to level up can attack, CA score = 100,010.
|
|
-- This means the attack will be done before the default AI attacks, so that AI
|
|
-- units do not get used otherwise by the default AI.
|
|
-- - If units that would not cause a leveling can also attack, CA score = 99,990,
|
|
-- meaning we see whether the default AI attacks that unit with one of those first.
|
|
-- We also check whether it is possible to move an own unit out of the way
|
|
--
|
|
-- Attack rating:
|
|
-- 0. If the CTD (chance to die) of the AI unit is larger than the value of
|
|
-- aggression for the side, do not do the attack
|
|
-- 1. Otherwise, if the attack might result in a kill, do that preferentially:
|
|
-- rating = CTD of defender - CTD of attacker
|
|
-- 2. Otherwise, if the enemy is poisoned, do not attack (wait for it
|
|
-- weaken and attack on a later turn)
|
|
-- 3. Otherwise, calculate damage done to enemy (as if it were not leveling) and
|
|
-- own unit, expressed in partial loss of unit value (gold) and minimize both.
|
|
-- Damage to enemy is minimized because we want to level it with the weakest AI unit,
|
|
-- so that we can follow up with stronger units. In addition, use of poison or
|
|
-- slow attacks is strongly discouraged. See code for exact equations.
|
|
|
|
local XP_attack
|
|
|
|
local ca_attack_highxp = {}
|
|
|
|
function ca_attack_highxp:evaluation(cfg, data, filter_own)
|
|
-- Note: (most of) the code below is set up to maximize speed. Do not
|
|
-- "simplify" this unless you understand exactly what that means
|
|
|
|
local attacks_aspect = ai.aspects.attacks
|
|
local max_unit_level = 0
|
|
local units = {}
|
|
for _,unit in ipairs(attacks_aspect.own) do
|
|
if (unit.attacks_left > 0) and (#unit.attacks > 0) and (not unit.canrecruit) and unit:matches(filter_own) then
|
|
table.insert(units, unit)
|
|
|
|
local level = unit.level
|
|
if (level > max_unit_level) then
|
|
max_unit_level = level
|
|
end
|
|
end
|
|
end
|
|
if (not units[1]) then return 0 end
|
|
|
|
-- Mark enemies as potential targets if they are close enough to an AI unit
|
|
-- that could trigger them leveling up; this is not a sufficient criterion,
|
|
-- but it is much faster than path finding, so it is done for preselection.
|
|
local target_infos = {}
|
|
for i_t,enemy in ipairs(attacks_aspect.enemy) do
|
|
local enemy_can_advance = true
|
|
if (not enemy.advances_to[1]) and (not wml.get_child(enemy.__cfg, 'advancement')) then
|
|
enemy_can_advance = false
|
|
end
|
|
|
|
if enemy_can_advance and AH.is_attackable_enemy(enemy) then
|
|
local XP_to_levelup = enemy.max_experience - enemy.experience
|
|
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 * wesnoth.game_config.combat_experience >= XP_to_levelup) then
|
|
potential_target = true
|
|
table.insert(ind_attackers, i_u)
|
|
else
|
|
table.insert(ind_other_units, i_u)
|
|
end
|
|
end
|
|
end
|
|
|
|
if potential_target then
|
|
local target_info = {
|
|
ind_target = i_t,
|
|
ind_attackers = ind_attackers,
|
|
ind_other_units = ind_other_units
|
|
}
|
|
table.insert(target_infos, target_info)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
if (not target_infos[1]) then return 0 end
|
|
|
|
-- The following location sets are used so that we at most need to call
|
|
-- find_reach() and unit:clone() once per unit
|
|
local reaches = LS.create()
|
|
local attacker_copies = LS.create()
|
|
|
|
local aggression = ai.aspects.aggression
|
|
local avoid_map = LS.of_pairs(ai.aspects.avoid)
|
|
local max_ca_score, max_rating, best_attack = 0, 0, nil
|
|
for _,target_info in ipairs(target_infos) do
|
|
wesnoth.interface.handle_user_interact()
|
|
local target = attacks_aspect.enemy[target_info.ind_target]
|
|
local can_force_level = {}
|
|
local attack_hexes = LS.create()
|
|
for xa,ya in wesnoth.current.map:iter_adjacent(target) do
|
|
if (not avoid_map:get(xa, ya)) then
|
|
local unit_in_way = wesnoth.units.get(xa, ya)
|
|
|
|
if AH.is_visible_unit(wesnoth.current.side, unit_in_way) then
|
|
if (unit_in_way.side == wesnoth.current.side) then
|
|
local uiw_reach
|
|
if reaches:get(unit_in_way.x, unit_in_way.y) then
|
|
uiw_reach = reaches:get(unit_in_way.x, unit_in_way.y)
|
|
else
|
|
uiw_reach = wesnoth.paths.find_reach(unit_in_way)
|
|
reaches:insert(unit_in_way.x, unit_in_way.y, uiw_reach)
|
|
end
|
|
|
|
-- Check whether the unit to move out of the way has an unoccupied hex to move to.
|
|
-- We do not deal with cases where a unit can move out of the way for a
|
|
-- unit that is moving out of the way of the initial unit (etc.).
|
|
local can_move = false
|
|
for _,uiw_loc in ipairs(uiw_reach) do
|
|
-- Unit in the way of the unit in the way
|
|
local uiw_uiw = wesnoth.units.get(uiw_loc[1], uiw_loc[2])
|
|
if (not AH.is_visible_unit(wesnoth.current.side, uiw_uiw)) then
|
|
can_move = true
|
|
break
|
|
end
|
|
end
|
|
if (not can_move) then
|
|
-- Keep this case as the unit in the way might be a potential attacker
|
|
attack_hexes:insert(xa, ya, unit_in_way.id)
|
|
else
|
|
attack_hexes:insert(xa, ya, 'can_move_away')
|
|
end
|
|
end
|
|
else
|
|
attack_hexes:insert(xa, ya, 'empty')
|
|
end
|
|
end
|
|
end
|
|
|
|
attack_hexes:iter(function(xa, ya, occupied)
|
|
for _,i_a in ipairs(target_info.ind_attackers) do
|
|
local attacker = units[i_a]
|
|
if (occupied == 'empty') or (occupied == 'can_move_away') then
|
|
-- If the hex is not blocked, check all potential attackers
|
|
local reach
|
|
if reaches:get(attacker.x, attacker.y) then
|
|
reach = reaches:get(attacker.x, attacker.y)
|
|
else
|
|
reach = wesnoth.paths.find_reach(attacker)
|
|
reaches:insert(attacker.x, attacker.y, reach)
|
|
end
|
|
|
|
for _,loc in ipairs(reach) do
|
|
if (loc[1] == xa) and (loc[2] == ya) then
|
|
local tmp = {
|
|
ind_attacker = i_a,
|
|
dst = { x = xa, y = ya },
|
|
src = { x = attacker.x, y = attacker.y }
|
|
}
|
|
table.insert(can_force_level, tmp)
|
|
break
|
|
end
|
|
end
|
|
else
|
|
-- If hex is blocked by own units, check whether this unit
|
|
-- is one of the potential attackers
|
|
if (attacker.id == occupied) then
|
|
local tmp = {
|
|
ind_attacker = i_a,
|
|
dst = { x = xa, y = ya },
|
|
src = { x = attacker.x, y = attacker.y }
|
|
}
|
|
|
|
table.insert(can_force_level, tmp)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- If a leveling attack is possible, check whether any of the other
|
|
-- units (those with too low a level to force leveling up) can get there
|
|
local ca_score = 100010
|
|
|
|
attack_hexes:iter(function(xa, ya, occupied)
|
|
if (ca_score == 100010) then -- cannot break out of the iteration with goto
|
|
for _,i_u in ipairs(target_info.ind_other_units) do
|
|
local unit = units[i_u]
|
|
if (occupied == 'empty') or (occupied == 'can_move_away') then
|
|
-- If the hex is not blocked, check if unit can get there
|
|
local reach
|
|
if reaches:get(unit.x, unit.y) then
|
|
reach = reaches:get(unit.x, unit.y)
|
|
else
|
|
reach = wesnoth.paths.find_reach(unit)
|
|
reaches:insert(unit.x, unit.y, reach)
|
|
end
|
|
|
|
for _,loc in ipairs(reach) do
|
|
if (loc[1] == xa) and (loc[2] == ya) then
|
|
ca_score = 99990
|
|
goto found_unit
|
|
end
|
|
end
|
|
else
|
|
-- If hex is blocked by own units, check whether this unit
|
|
-- is one of the potential attackers
|
|
if (unit.id == occupied) then
|
|
ca_score = 99990
|
|
goto found_unit
|
|
end
|
|
end
|
|
end
|
|
-- It is sufficient to find one unit that can get to any attack hex
|
|
::found_unit::
|
|
end
|
|
end)
|
|
|
|
if (ca_score >= max_ca_score) then
|
|
for _,attack_info in ipairs(can_force_level) do
|
|
local attacker = units[attack_info.ind_attacker]
|
|
local attacker_copy
|
|
if attacker_copies:get(attacker.x, attacker.y) then
|
|
attacker_copy = attacker_copies:get(attacker.x, attacker.y)
|
|
else
|
|
attacker_copy = attacker:clone()
|
|
attacker_copies:insert(attacker.x, attacker.y, attacker_copy)
|
|
end
|
|
|
|
attacker_copy.x = attack_info.dst.x
|
|
attacker_copy.y = attack_info.dst.y
|
|
|
|
-- Choose the attacker that would do the *least* damage.
|
|
-- We want the damage distribution here as if the target were not to level up
|
|
-- the chance to die is the same in either case
|
|
local old_experience = target.experience
|
|
target.experience = 0
|
|
local att_stats, def_stats, att_weapon = wesnoth.simulate_combat(attacker_copy, target)
|
|
target.experience = old_experience
|
|
|
|
local rating = -1000
|
|
if (att_stats.hp_chance[0] <= aggression) then
|
|
if (def_stats.hp_chance[0] > 0) then
|
|
rating = 5000 + def_stats.hp_chance[0] - att_stats.hp_chance[0]
|
|
elseif target.status.poisoned then
|
|
rating = -1002
|
|
else
|
|
rating = 1000
|
|
|
|
local enemy_value_loss = (target.hitpoints - def_stats.average_hp) / target.max_hitpoints
|
|
enemy_value_loss = enemy_value_loss * target.cost
|
|
|
|
-- We want the _least_ damage to the enemy, so the minus sign is no typo!
|
|
rating = rating - enemy_value_loss
|
|
|
|
local own_value_loss = (attacker_copy.hitpoints - att_stats.average_hp) / attacker_copy.max_hitpoints
|
|
own_value_loss = own_value_loss + att_stats.hp_chance[0]
|
|
own_value_loss = own_value_loss * attacker_copy.cost
|
|
|
|
rating = rating - own_value_loss
|
|
|
|
-- Strongly discourage poison or slow attacks
|
|
if att_weapon.poisons or att_weapon.slows then
|
|
rating = rating - 100
|
|
end
|
|
|
|
-- Minor penalty if the attack hex is occupied
|
|
if (attack_hexes:get(attack_info.dst.x, attack_info.dst.y) == 'can_move_away') then
|
|
rating = rating - 0.001
|
|
end
|
|
end
|
|
end
|
|
|
|
if (rating > max_rating)
|
|
or ((rating > 0) and (ca_score > max_ca_score))
|
|
then
|
|
max_rating = rating
|
|
max_ca_score = ca_score
|
|
best_attack = attack_info
|
|
best_attack.target = { x = target.x, y = target.y }
|
|
best_attack.ca_score = ca_score
|
|
-- Also need to save weapon number because attack simulation above
|
|
-- is for a different situation than the actual units on the map.
|
|
-- +1 because of difference between Lua and C++ indices
|
|
best_attack.attack_num = att_weapon.attack_num + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
end
|
|
|
|
if best_attack then
|
|
XP_attack = best_attack
|
|
end
|
|
|
|
return max_ca_score
|
|
end
|
|
|
|
function ca_attack_highxp:execution(cfg, data)
|
|
AH.robust_move_and_attack(ai, XP_attack.src, XP_attack.dst, XP_attack.target, { weapon = XP_attack.attack_num })
|
|
XP_attack = nil
|
|
end
|
|
|
|
return ca_attack_highxp
|