Protect Unit Micro AI: code cleanup

This commit is contained in:
mattsc 2014-04-16 08:07:07 -07:00
parent 8fa3a60e56
commit f6830897aa
3 changed files with 67 additions and 123 deletions

View file

@ -6,116 +6,96 @@ local ca_protect_unit_attack = {}
function ca_protect_unit_attack:evaluation(ai, cfg, self)
-- Find possible attacks for the units
-- This is set up very conservatively
-- If unit can die in the worst case, it is not done, even if _really_ unlikely
-- This is set up very conservatively: if a unit can die in the attack
-- or the counter attack on the enemy turn, it does not attack, even if that's really unlikely
local units = {}
for i,id in ipairs(cfg.id) do
for _,id in ipairs(cfg.id) do
table.insert(units, AH.get_units_with_attacks { id = id }[1])
end
if (not units[1]) then return 0 end
local attacks = AH.get_attacks(units, { simulate_combat = true })
if (not attacks[1]) then return 0 end
--print('#attacks',#attacks,ids)
-- All enemy units
local enemies = wesnoth.get_units {
{ "filter_side", {{"enemy_of", {side = wesnoth.current.side} }} }
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } }
}
-- For retaliation calculation:
-- Find all hexes enemies can attack on their next turn
-- Counter attack calculation
local enemy_attacks = {}
for i,e in ipairs(enemies) do
local attack_map = BC.get_attack_map_unit(e).units
table.insert(enemy_attacks, { enemy = e, attack_map = attack_map })
for _,enemy in ipairs(enemies) do
local attack_map = BC.get_attack_map_unit(enemy).units
table.insert(enemy_attacks, { enemy = enemy, attack_map = attack_map })
end
-- Set up a retaliation table, as many pairs of attacks will be the same
local retal_table = {}
-- Set up a counter attack damage table, as many pairs of attacks will be the same
local counter_damage_table = {}
local max_rating, best_attack = -9e99, {}
for i,a in pairs(attacks) do
--print(i,a.dst.x,a.dst.y)
--print(' chance to die:',a.att_stats.hp_chance[0])
-- Only consider if there is no chance to die or to be poisoned or slowed
if ((a.att_stats.hp_chance[0] == 0) and (a.att_stats.poisoned == 0) and (a.att_stats.slowed == 0)) then
-- Get maximum possible retaliation possible by enemies on next turn
local my_unit = wesnoth.get_unit(a.src.x, a.src.y)
local max_retal = 0
for j,ea in ipairs(enemy_attacks) do
local can_attack = ea.attack_map:get(a.dst.x, a.dst.y)
if can_attack then
local max_rating, best_attack = -9e99
for _,attack in pairs(attacks) do
-- Only consider attack if there is no chance to die or to be poisoned or slowed
if (attack.att_stats.hp_chance[0] == 0)
and (attack.att_stats.poisoned == 0)
and (attack.att_stats.slowed == 0)
then
-- Get maximum possible counter attack damage by enemies on their turn
local unit = wesnoth.get_unit(attack.src.x, attack.src.y)
local max_counter_damage = 0
for _,enemy_attack in ipairs(enemy_attacks) do
if enemy_attack.attack_map:get(attack.dst.x, attack.dst.y) then
-- Check first if this attack combination has already been calculated
local str = (a.src.x + a.src.y * 1000) .. '-' .. (a.target.x + a.target.y * 1000)
--print(str)
if retal_table[str] then -- If so, use saved value
--print(' retal already calculated: ',str,retal_table[str])
max_retal = max_retal + retal_table[str]
local str = (attack.src.x + attack.src.y * 1000) .. '-' .. (attack.target.x + attack.target.y * 1000)
if counter_damage_table[str] then -- If so, use saved value
max_counter_damage = max_counter_damage + counter_damage_table[str]
else -- if not, calculate it and save value
-- Go thru all weapons, as "best weapon" might be different later on
local n_weapon = 0
local min_hp = my_unit.hitpoints
for weapon in H.child_range(ea.enemy.__cfg, "attack") do
local min_hp = unit.hitpoints
for weapon in H.child_range(enemy_attack.enemy.__cfg, "attack") do
n_weapon = n_weapon + 1
-- Terrain does not matter for this, we're only interested in the maximum damage
local att_stats, def_stats = wesnoth.simulate_combat(ea.enemy, n_weapon, my_unit)
local att_stats, def_stats = wesnoth.simulate_combat(enemy_attack.enemy, n_weapon, unit)
-- Find minimum HP of our unit
-- find the minimum hp outcome
-- Note: cannot use ipairs() because count starts at 0
local min_hp_weapon = my_unit.hitpoints
local min_hp_weapon = unit.hitpoints
for hp,chance in pairs(def_stats.hp_chance) do
if ((chance > 0) and (hp < min_hp_weapon)) then
if (chance > 0) and (hp < min_hp_weapon) then
min_hp_weapon = hp
end
end
if (min_hp_weapon < min_hp) then min_hp = min_hp_weapon end
end
--print(' min_hp:',min_hp, ' max damage:',my_unit.hitpoints-min_hp)
max_retal = max_retal + my_unit.hitpoints - min_hp
retal_table[str] = my_unit.hitpoints - min_hp
max_counter_damage = max_counter_damage + unit.hitpoints - min_hp
counter_damage_table[str] = unit.hitpoints - min_hp
end
end
end
--print(' max retaliation:',max_retal)
-- and add this to damage possible on this attack
-- Note: cannot use ipairs() because count starts at 0
-- Add this to damage possible on this attack
local min_hp = 1000
for hp,chance in pairs(a.att_stats.hp_chance) do
--print(hp,chance)
if ((chance > 0) and (hp < min_hp)) then
for hp,chance in pairs(attack.att_stats.hp_chance) do
if (chance > 0) and (hp < min_hp) then
min_hp = hp
end
end
local min_outcome = min_hp - max_retal
--print(' min hp this attack:',min_hp)
--print(' ave hp defender: ',a.def_stats.average_hp)
--print(' min_outcome',min_outcome)
local min_outcome = min_hp - max_counter_damage
-- If this is >0, consider the attack
-- If this is > 0, consider the attack
if (min_outcome > 0) then
local rating = min_outcome + a.att_stats.average_hp - a.def_stats.average_hp
--print(' rating:',rating,' min_outcome',min_outcome)
local rating = min_outcome + attack.att_stats.average_hp - attack.def_stats.average_hp
if (rating > max_rating) then
max_rating, best_attack = rating, a
max_rating, best_attack = rating, attack
end
end
end
end
--print('Max_rating:', max_rating)
if (max_rating > -9e99) then
if best_attack then
self.data.PU_best_attack = best_attack
return 95000
end

View file

@ -20,8 +20,7 @@ end
function ca_protect_unit_finish:execution(ai, cfg, self)
AH.movefull_stopunit(ai, self.data.PU_unit, self.data.PU_goal)
self.data.PU_unit = nil
self.data.PU_goal = nil
self.data.PU_unit, self.data.PU_goal = nil, nil
end
return ca_protect_unit_finish

View file

@ -5,7 +5,7 @@ local BC = wesnoth.require "ai/lua/battle_calcs.lua"
local function get_protected_units(cfg)
local units = {}
for i,id in ipairs(cfg.id) do
for _,id in ipairs(cfg.id) do
table.insert(units, AH.get_units_with_moves { id = id }[1])
end
return units
@ -20,74 +20,51 @@ end
function ca_protect_unit_move:execution(ai, cfg, self)
-- Find and execute best (safest) move toward goal
local units = get_protected_units(cfg)
local protected_units = get_protected_units(cfg)
-- Need to take the units off the map, as they don't count into the map scores
-- (as long as they can still move)
for i,u in ipairs(units) do wesnoth.extract_unit(u) end
-- Need to take the protected units off the map, as they don't count into the map scores
-- as long as they can still move
for _,unit in ipairs(protected_units) do wesnoth.extract_unit(unit) end
-- All the units on the map
-- Counts all enemies, but only own units (not allies)
local my_units = wesnoth.get_units {side = wesnoth.current.side}
local units = wesnoth.get_units { side = wesnoth.current.side }
local enemy_units = wesnoth.get_units {
{ "filter_side", {{"enemy_of", {side = wesnoth.current.side} }} }
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } }
}
-- My attack map
local MAM = BC.get_attack_map(my_units).units -- enemy attack map
--AH.put_labels(MAM)
local attack_map = BC.get_attack_map(units).units -- enemy attack map
local enemy_attack_map = BC.get_attack_map(enemy_units).units -- enemy attack map
-- Enemy attack map
local EAM = BC.get_attack_map(enemy_units).units -- enemy attack map
--AH.put_labels(EAM)
-- Now put the units back out there
for i,u in ipairs(units) do wesnoth.put_unit(u) end
-- Now put the protected units back out there
for _,unit in ipairs(protected_units) do wesnoth.put_unit(unit) end
-- We move the weakest (fewest HP unit) first
local unit = AH.choose(units, function(tmp) return -tmp.hitpoints end)
--print("Moving: ",unit.id)
-- Also need the goal for this unit
local unit = AH.choose(protected_units, function(u) return - u.hitpoints end)
local goal = {}
for i,id in ipairs(cfg.id) do
if (unit.id == id) then goal = { cfg.goal_x[i], cfg.goal_y[i] } end
end
--print("Goal:",goal[1],goal[2])
-- Reachable hexes
local reach_map = AH.get_reachable_unocc(unit)
--AH.put_labels(reach_map)
local enemy_inverse_distance_map = AH.inverse_distance_map(enemy_units, reach_map)
-- Now calculate the enemy inverse distance map
-- This is done here because we only need it for the hexes the unit can reach
-- Enemy distance map
local EIDM = AH.inverse_distance_map(enemy_units, reach_map)
--AH.put_labels(EIDM)
-- Get a terrain defense map of reachable hexes
local TDM = LS.create()
local terrain_defense_map = LS.create()
reach_map:iter(function(x, y, data)
TDM:insert(x, y, 100 - wesnoth.unit_defense(unit, wesnoth.get_terrain(x, y)))
terrain_defense_map:insert(x, y, 100 - wesnoth.unit_defense(unit, wesnoth.get_terrain(x, y)))
end)
--AH.put_labels(TDM)
-- And finally, the goal distance map
local GDM = LS.create()
local goal_distance_map = LS.create()
reach_map:iter(function(x, y, data)
GDM:insert(x, y, H.distance_between(x, y, goal[1], goal[2]))
goal_distance_map:insert(x, y, H.distance_between(x, y, goal[1], goal[2]))
end)
--AH.put_labels(GDM)
-- Configuration parameters (no option to change these enabled at the moment)
local enemy_weight = self.data.PU_enemy_weight or 100.
local my_unit_weight = self.data.PU_my_unit_weight or 1.
local distance_weight = self.data.PU_distance_weight or 3.
local terrain_weight = self.data.PU_terrain_weight or 0.1
local bearing = self.data.PU_bearing or 1
-- If there are no enemies left, only distance to goal matters
-- This is to avoid rare situations where moving toward goal is canceled by moving away from own units
-- This is to avoid rare situations where moving toward goal rating is canceled by rating for moving away from own units
if (not enemy_units[1]) then
enemy_weight = 0
my_unit_weight = 0
@ -95,34 +72,22 @@ function ca_protect_unit_move:execution(ai, cfg, self)
terrain_weight = 0
end
local max_rating, best_hex = -9e99, -1
local rating_map = LS.create() -- Also set up rating map, so that it can be displayed
for ind,r in pairs(reach_map.values) do
-- Most important: stay away from enemy: default weight=100 per enemy unit
-- Staying close to own troops: default weight=1 per own unit (allies don't count)
local max_rating, best_hex = -9e99
for ind,_ in pairs(reach_map.values) do
local rating =
(MAM.values[ind] or 0) * my_unit_weight
- (EAM.values[ind] or 0) * enemy_weight
(attack_map.values[ind] or 0) * my_unit_weight
- (enemy_attack_map.values[ind] or 0) * enemy_weight
-- Distance to goal is second most important thing: weight=3 per hex
rating = rating - GDM.values[ind] * distance_weight
-- Note: rating will usually be negative, but that's ok (the least negative hex wins)
rating = rating - goal_distance_map.values[ind] * distance_weight
-- Terrain rating. Difference of 30 in defense should be worth ~1 step toward goal
rating = rating + TDM.values[ind] * terrain_weight
rating = rating + terrain_defense_map.values[ind] * terrain_weight
-- Tie breaker: closer to or farther from enemy
rating = rating + (EIDM.values[ind] or 0) / 10. * bearing
rating = rating + (enemy_attack_map.values[ind] or 0) / 10.
if (rating > max_rating) then
max_rating, best_hex = rating, ind
end
rating_map.values[ind] = rating
end
--AH.put_labels(rating_map)
--print("Best rating, hex:", max_rating, best_hex)
AH.movefull_stopunit(ai, unit, AH.get_LS_xy(best_hex))
end