Skip to content

Commit

Permalink
Fast Micro AI: code cleanup
Browse files Browse the repository at this point in the history
Most of the Fast MAI attack utils had been taken from a more general codebase and included things not needed here. This AI is supposed to be a slimmed down version doing only the absolutely necessary in as fast as possible a fashion.

(cherry-picked from commit f4f2a44)
  • Loading branch information
mattsc committed Oct 7, 2018
1 parent 02c830a commit 613ccd8
Show file tree
Hide file tree
Showing 4 changed files with 29 additions and 89 deletions.
103 changes: 23 additions & 80 deletions data/ai/micro_ais/cas/ca_fast_attack_utils.lua
Expand Up @@ -87,15 +87,12 @@ function ca_fast_attack_utils.single_unit_info(unit_proxy)
-- Collects unit information from proxy unit table @unit_proxy into a Lua table
-- so that it is accessible faster.
-- Note: Even accessing the directly readable fields of a unit proxy table
-- is slower than reading from a Lua table; not even talking about unit_proxy.__cfg.
-- is slower than reading from a smaller Lua table.
--
-- Important: this is slow, so it should only be called as needed,
-- but it does need to be redone after each move, as it contains
-- information like HP and XP (or the unit might have level up or been changed
-- information like HP and XP (or the unit might have leveled up or been changed
-- in an event).
-- Difference from the grunt rush version: also include x and y

local unit_cfg = unit_proxy.__cfg

local single_unit_info = {
id = unit_proxy.id,
Expand All @@ -110,10 +107,8 @@ function ca_fast_attack_utils.single_unit_info(unit_proxy)
experience = unit_proxy.experience,
max_experience = unit_proxy.max_experience,

alignment = unit_cfg.alignment,
tod_bonus = AH.get_unit_time_of_day_bonus(unit_cfg.alignment, wesnoth.get_time_of_day().lawful_bonus),
cost = unit_cfg.cost,
level = unit_cfg.level
cost = unit_proxy.cost,
level = unit_proxy.level
}

-- Include the ability type, such as: hides, heals, regenerate, skirmisher (set up as 'hides = true')
Expand All @@ -124,49 +119,6 @@ function ca_fast_attack_utils.single_unit_info(unit_proxy)
end
end

-- Information about the attacks indexed by weapon number,
-- including specials (e.g. 'poison = true')
single_unit_info.attacks = {}
for attack in wml.child_range(unit_cfg, 'attack') do
-- Extract information for specials; we do this first because some
-- custom special might have the same name as one of the default scalar fields
local a = {}
for special in wml.child_range(attack, 'specials') do
for _,sp in ipairs(special) do
if (sp[1] == 'damage') then -- this is 'backstab'
if (sp[2].id == 'backstab') then
a.backstab = true
else
if (sp[2].id == 'charge') then a.charge = true end
end
else
-- magical, marksman, custom chance-to-hit specials
if (sp[1] == 'chance_to_hit') then
a[sp[2].id or 'no_id'] = true
else
a[sp[1]] = true
end
end
end
end

-- Now extract the scalar (string and number) values from attack
for k,v in pairs(attack) do
if (type(v) == 'number') or (type(v) == 'string') then
a[k] = v
end
end

table.insert(single_unit_info.attacks, a)
end

-- Resistances to the 6 default attack types
local attack_types = { "arcane", "blade", "cold", "fire", "impact", "pierce" }
single_unit_info.resistances = {}
for _,attack_type in ipairs(attack_types) do
single_unit_info.resistances[attack_type] = unit_proxy:resistance(attack_type) / 100.
end

return single_unit_info
end

Expand Down Expand Up @@ -216,27 +168,25 @@ function ca_fast_attack_utils.get_unit_defense(unit_copy, x, y, defense_maps)
return defense_maps[unit_copy.id][x][y].defense
end

function ca_fast_attack_utils.is_acceptable_attack(damage_taken, damage_done, own_value_weight)
function ca_fast_attack_utils.is_acceptable_attack(damage_taken, damage_done, aggression)
-- Evaluate whether an attack is acceptable, based on the damage taken/done ratio
--
-- Inputs:
-- @damage_taken, @damage_done: should be in gold units as returned by ca_fast_attack_utils.attack_rating
-- This could be either the attacker (for taken) and defender (for done) rating of a single attack (combo)
-- or the overall attack (for done) and counter attack rating (for taken)
-- @own_value_weight (optional): value for the minimum ratio of done/taken that is acceptable

own_value_weight = own_value_weight or 0.6 -- equivalent to aggression = 0.4 (default mainline value)
-- @aggression: value determining which ratio of damage done/taken that is acceptable

-- Otherwise it depends on whether the numbers are positive or negative
-- Negative damage means that one or several of the units are likely to level up
if (damage_taken < 0) and (damage_done < 0) then
return (damage_done / damage_taken) >= own_value_weight
return (damage_done / damage_taken) >= (1 - aggression)
end

if (damage_taken <= 0) then damage_taken = 0.001 end
if (damage_done <= 0) then damage_done = 0.001 end

return (damage_done / damage_taken) >= own_value_weight
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)
Expand All @@ -247,18 +197,15 @@ function ca_fast_attack_utils.damage_rating_unit(attacker_info, defender_info, a
-- Note: damage is damage TO the attacker, not damage done BY the attacker
--
-- Input parameters:
-- @attacker_info, @defender_info: unit_info tables produced by ca_fast_gamestate_utils.single_unit_info()
-- @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
-- Set to nil or false if not, to anything if it is a village (does not have to be a boolean)
--
-- Optional parameters:
-- @cfg: the optional different weights listed right below
-- Note: this is currently not used in the Fast MAI, but kept in as a hook for potential upgrades
-- @cfg: the optional weights listed right below (currently only leader_weight)

local leader_weight = (cfg and cfg.leader_weight) or 2.
local xp_weight = (cfg and cfg.xp_weight) or 1.
local level_weight = (cfg and cfg.level_weight) or 1.

local damage = attacker_info.hitpoints - att_stat.average_hp

Expand Down Expand Up @@ -301,7 +248,7 @@ function ca_fast_attack_utils.damage_rating_unit(attacker_info, defender_info, a
level_bonus = (1. - att_stat.hp_chance[0]) * def_stat.hp_chance[0]
end

fractional_damage = fractional_damage - level_bonus * level_weight
fractional_damage = fractional_damage - level_bonus

-- Now convert this into gold-equivalent value
local value = attacker_info.cost
Expand All @@ -312,9 +259,8 @@ function ca_fast_attack_utils.damage_rating_unit(attacker_info, defender_info, a
end

-- Being closer to leveling makes the attacker more valuable
-- TODO: consider using a more precise measure here
local xp_bonus = attacker_info.experience / attacker_info.max_experience
value = value * (1. + xp_bonus * xp_weight)
value = value * (1. + xp_bonus)

local rating = fractional_damage * value

Expand All @@ -331,11 +277,12 @@ function ca_fast_attack_utils.attack_rating(attacker_infos, defender_info, dsts,
-- @att_stats: array of the attack stats of the attack combination(!) of the attackers
-- (must be an array even for single unit attacks)
-- @def_stat: the combat stats of the defender after facing the combination of the attackers
-- @gamedata: table with the game state as produced by ca_fast_gamestate_utils.gamedata()
-- @gamedata: table with the game state as produced by ca_fast_attack_utils.gamedata()
--
-- Optional inputs:
-- @cfg: the different weights listed right below
-- Note: this is currently not used in the Fast MAI, but kept in as a hook for potential upgrades
-- @cfg: table with optional configuration parameters:
-- - aggression: the default aggression aspect, determining how to balance own vs. enemy damage
-- - leader_weight: to be passed on to damage_rating_unit()
--
-- Returns:
-- - Overall rating for the attack or attack combo
Expand All @@ -344,14 +291,10 @@ function ca_fast_attack_utils.attack_rating(attacker_infos, defender_info, dsts,
-- - Extra rating: additional ratings that do not directly describe damage
-- This should be used to help decide which attack to pick,
-- but not for, e.g., evaluating counter attacks (which should be entirely damage based)
-- Note: rating = defender_rating - attacker_rating * own_value_weight + extra_rating
-- Note: rating = defender_rating - attacker_rating * (1 - aggression) + extra_rating

-- Set up the config parameters for the rating
local defender_starting_damage_weight = (cfg and cfg.defender_starting_damage_weight) or 0.33
local defense_weight = (cfg and cfg.defense_weight) or 0.1
local distance_leader_weight = (cfg and cfg.distance_leader_weight) or 0.002
local occupied_hex_penalty = (cfg and cfg.occupied_hex_penalty) or 0.001
local own_value_weight = (cfg and cfg.own_value_weight) or 1.0
local aggression = (cfg and cfg.aggression) or 0.4

local attacker_rating = 0
for i,attacker_info in ipairs(attacker_infos) do
Expand All @@ -377,7 +320,7 @@ function ca_fast_attack_utils.attack_rating(attacker_infos, defender_info, dsts,

-- Prefer to attack already damaged enemies
local defender_starting_damage_fraction = defender_info.max_hitpoints - defender_info.hitpoints
extra_rating = extra_rating + defender_starting_damage_fraction * defender_starting_damage_weight
extra_rating = extra_rating + defender_starting_damage_fraction * 0.33

-- 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)
Expand All @@ -399,7 +342,7 @@ function ca_fast_attack_utils.attack_rating(attacker_infos, defender_info, dsts,
gamedata.defense_maps
)
end
defense_rating = defense_rating / #dsts * defense_weight
defense_rating = defense_rating / #dsts * 0.1

extra_rating = extra_rating + defense_rating

Expand All @@ -415,14 +358,14 @@ function ca_fast_attack_utils.attack_rating(attacker_infos, defender_info, dsts,
- M.distance_between(dst[1], dst[2], leader_x, leader_y)
rel_dist_rating = rel_dist_rating + relative_distance
end
rel_dist_rating = rel_dist_rating / #dsts * distance_leader_weight
rel_dist_rating = rel_dist_rating / #dsts * 0.002

extra_rating = extra_rating + rel_dist_rating
end

-- Finally add up and apply factor of own unit weight to defender unit weight
-- This is a number equivalent to 'aggression' in the default AI (but not numerically equal)
local rating = defender_rating - attacker_rating * own_value_weight + extra_rating
local rating = defender_rating - attacker_rating * (1 - aggression) + extra_rating

return rating, attacker_rating, defender_rating, extra_rating
end
Expand All @@ -436,7 +379,7 @@ function ca_fast_attack_utils.battle_outcome(attacker_copy, defender_proxy, dst,
-- @dst: location from which the attacker will attack in form { x, y }
-- @attacker_info, @defender_info: unit info for the two units (needed in addition to the units
-- themselves in order to speed things up)
-- @gamedata: table with the game state as produced by ca_fast_gamestate_utils.gamedata()
-- @gamedata: table with the game state as produced by ca_fast_attack_utils.gamedata()
-- @move_cache: for caching data *for this move only*, needs to be cleared after a gamestate change

local defender_defense = ca_fast_attack_utils.get_unit_defense(defender_proxy, defender_proxy.x, defender_proxy.y, gamedata.defense_maps)
Expand Down
5 changes: 2 additions & 3 deletions data/ai/micro_ais/cas/ca_fast_combat.lua
Expand Up @@ -71,7 +71,6 @@ function ca_fast_combat:evaluation(cfg, data)

local aggression = ai.aspects.aggression
if (aggression > 1) then aggression = 1 end
local own_value_weight = 1. - aggression

-- Get the locations to be avoided
local avoid_map = FAU.get_avoid_map(cfg)
Expand Down Expand Up @@ -102,12 +101,12 @@ function ca_fast_combat:evaluation(cfg, data)
{ unit_info }, target_info, { { attack.dst.x, attack.dst.y } },
{ att_stat }, def_stat, data.gamedata,
{
own_value_weight = own_value_weight,
aggression = aggression,
leader_weight = cfg.leader_weight
}
)

local acceptable_attack = FAU.is_acceptable_attack(attacker_rating, defender_rating, own_value_weight)
local acceptable_attack = FAU.is_acceptable_attack(attacker_rating, defender_rating, aggression)

if acceptable_attack and (rating > max_rating) then
max_rating, best_target, best_dst = rating, target, attack.dst
Expand Down
7 changes: 3 additions & 4 deletions data/ai/micro_ais/cas/ca_fast_combat_leader.lua
Expand Up @@ -64,7 +64,6 @@ function ca_fast_combat_leader:evaluation(cfg, data)

local aggression = ai.aspects.aggression
if (aggression > 1) then aggression = 1 end
local own_value_weight = 1. - aggression

-- Get the locations to be avoided
local avoid_map = FAU.get_avoid_map(cfg)
Expand Down Expand Up @@ -161,12 +160,12 @@ function ca_fast_combat_leader:evaluation(cfg, data)
{ leader_info }, target_info, { { attack.dst.x, attack.dst.y } },
{ att_stat }, def_stat, data.gamedata,
{
own_value_weight = own_value_weight,
leader_weight = cfg.leader_weight
aggression = aggression,
leader_weight = leader_weight
}
)

acceptable_attack = FAU.is_acceptable_attack(attacker_rating, defender_rating, own_value_weight)
acceptable_attack = FAU.is_acceptable_attack(attacker_rating, defender_rating, aggression)

if acceptable_attack and (rating > max_rating) then
max_rating, best_target, best_dst = rating, target, attack.dst
Expand Down
3 changes: 1 addition & 2 deletions data/ai/micro_ais/cas/ca_fast_move.lua
Expand Up @@ -263,8 +263,7 @@ function ca_fast_move:execution(cfg)
end
end

unit.x, unit.y = old_x, old_y
unit:to_map()
unit:to_map(old_x, old_y)
end

if best_hex then
Expand Down

0 comments on commit 613ccd8

Please sign in to comment.