Protect Unit Micro AI: code cleanup
This commit is contained in:
parent
04bf7c56e0
commit
3b8b923d7f
3 changed files with 67 additions and 123 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue