battle_calcs library: add functions best_defense_map() and get_attack_combos_subset()

This commit is contained in:
mattsc 2013-05-03 07:06:22 -07:00
parent 5fb4dd3816
commit e7a71a51d7

View file

@ -1317,4 +1317,309 @@ function battle_calcs.relative_damage_map(units, enemies, cache)
return damage_map, own_damage_map, enemy_damage_map
end
function battle_calcs.best_defense_map(units, cfg)
-- Get a defense rating map of all hexes all units in 'units' can reach
-- For each hex, the value is the maximum of any of the units that can reach that hex
-- cfg: table with config parameters
-- max_moves: if set use max_moves for units (this setting is always used for units on other sides)
cfg = cfg or {}
local defense_map = LS.create()
for i,u in ipairs(units) do
-- Set max_moves according to the cfg value
local max_moves = cfg.max_moves
-- For unit on other than current side, only max_moves=true makes sense
if (u.side ~= wesnoth.current.side) then max_moves = true end
local old_moves = u.moves
if max_moves then u.moves = u.max_moves end
local reach = wesnoth.find_reach(u, cfg)
if max_moves then u.moves = old_moves end
for j,r in ipairs(reach) do
local defense = 100 - wesnoth.unit_defense(u, wesnoth.get_terrain(r[1], r[2]))
if (defense > (defense_map:get(r[1], r[2]) or -9e99)) then
defense_map:insert(r[1], r[2], defense)
end
end
end
return defense_map
end
function battle_calcs.get_attack_combos_subset(units, enemy, cfg)
-- Calculate combinations of attacks by 'units' on 'enemy'
-- This method does *not* produce all possible attack combinations, but is
-- meant to have a good chance to find either the best combination,
-- or something close to it, by only considering a subset of all possibilities.
-- It is also configurable to stop accumulating combinations when certain criteria are met.
--
-- The return value is an array of attack combinations, where each element is another
-- array of tables containing 'dst' and 'src' fields of the attacking units. It can be
-- specified whether the order of the attacks matters or not (see below).
--
-- Note: This function is optimized for speed, not elegance
--
-- Note 2: the structure of the returned table is different from the (current) return value
-- of ai_helper.get_attack_combos(), since the order of attacks never matters for the latter.
-- TODO: consider making the two consistent (not sure yet whether that is advantageous)
--
-- cfg: Optional table of optional configuration parameters
-- - order_matters: if set, keep attack combos that use the same units on the same
-- hexes, but in different attack order (default: false)
-- - max_combos: stop adding attack combos if this number of combos has been reached
-- default: assemble all possible combinations
-- - max_time: stop adding attack combos if this much time (in seconds) has passed
-- default: assemble all possible combinations
-- note: this counts the time from the first call to add_attack(), not to
-- get_attack_combos_cfg(), so there's a bit of extra overhead in here.
-- This is done to prevent the return of no combos at all
-- Note 2: there is some overhead involved in reading the time from the system,
-- so don't use this unless it's needed
-- - skip_presort: by default, the units are presorted in order of the unit with
-- the highest rating first. This has the advantage of likely finding the best
-- (or at least close to the best) attack combination earlier, but it add overhead,
-- so it's actually a disadvantage for small numbers of combinations. skip_presort
-- specifies the number of units up to which the presorting is skipped. Default: 5
cfg = cfg or {}
cfg.order_matters = cfg.order_matters or false
cfg.max_combos = cfg.max_combos or 9e99
cfg.max_time = cfg.max_time or false
cfg.skip_presort = cfg.skip_presort or 5
----- begin add_attack() -----
-- Recursive local function adding another attack to the current combo
-- and adding the current combo to the overall attack_combos array
local function add_attack(attacks, reachable_hexes, n_reach, attack_combos, combos_str, current_combo, hexes_used, cfg)
local time_up = false
if cfg.max_time and (wesnoth.get_time_stamp() / 1000. - cfg.start_time >= cfg.max_time) then
time_up = true
end
-- Go through all the units
for i,a in ipairs(attacks) do -- 'a' is array of all attacks for the unit
-- Then go through the individual attacks of the unit ...
for j,l in ipairs(a) do
-- But only if this hex is not used yet and
-- the cutoff criteria are not met
if (not hexes_used[l.dst]) and (not time_up) and (#attack_combos < cfg.max_combos) then
-- Mark this hex as used by this unit
hexes_used[l.dst] = a.src
-- Set up a string uniquely identifying the unit/attack hex pairs
-- for current_combo. This is used to exclude pairs that already
-- exist in a different order (if 'cfg.order_matters' is not set)
-- For this, we also add the numerical value of the attack_hex to
-- the 'hexes_used' table (in addition to the line above)
local str = ''
if (not cfg.order_matters) then
hexes_used[reachable_hexes[l.dst]] = a.src
for h = 1, n_reach do
if hexes_used[h] then
str = str .. hexes_used[h] .. '-'
else
str = str .. '0-'
end
end
end
-- 'combos_str' contains all the strings of previous combos
-- (if 'cfg.order_matters' is not set)
-- Only add this combo if it does not yet exist
if (not combos_str[str]) then
-- Add the string identifyer to the array
if (not cfg.order_matters) then
combos_str[str] = true
end
-- Add the attack to 'current_combo'
table.insert(current_combo, { dst = l.dst, src = a.src })
-- And *copy* the content of 'current_combo' into 'attack_combos'
local n_combos = #attack_combos + 1
attack_combos[n_combos] = {}
for i,c in pairs(current_combo) do attack_combos[n_combos][i] = c end
-- Finally, remove the current unit for 'attacks' for the call to the next recursion level
table.remove(attacks, i)
add_attack(attacks, reachable_hexes, n_reach, attack_combos, combos_str, current_combo, hexes_used, cfg)
-- Reinsert the unit
table.insert(attacks, i, a)
-- And remove the last element (current attack) from 'current_combo'
table.remove(current_combo)
end
-- And mark the hex as usable again
if (not cfg.order_matters) then
hexes_used[reachable_hexes[l.dst]] = nil
end
hexes_used[l.dst] = nil
-- *** Important ***: We *only* consider one attack hex per unit, the
-- first that is found in the array of attacks for the unit. As they
-- are sorted by terrain defense, we simply use the first in the table
-- the unit can reach that is not occupied
-- That's what the 'break' does here:
break
end
end
end
end
----- end add_attack() -----
-- For units on the current side, we need to make sure that
-- there isn't a unit of the same side in the way that cannot move any more
-- Set up an array of hexes blocked in such a way
-- For units on other sides we always assume that they can move away
local blocked_hexes = LS.create()
if units[1] and (units[1].side == wesnoth.current.side) then
for x, y in H.adjacent_tiles(enemy.x, enemy.y) do
local unit_in_way = wesnoth.get_unit(x,y)
if unit_in_way then
-- Units on the same side are blockers if they cannot move away
if (unit_in_way.side == wesnoth.current.side) then
local reach = wesnoth.find_reach(unit_in_way)
if (#reach <= 1) then
blocked_hexes:insert(unit_in_way.x, unit_in_way.y)
end
else -- Units on other sides are always blockers
blocked_hexes:insert(unit_in_way.x, unit_in_way.y)
end
end
end
end
--DBG.dbms(blocked_hexes)
-- For sides other than the current, we always use max_moves,
-- for the current side we always use current moves
local old_moves = {}
for i,u in ipairs(units) do
if (u.side ~= wesnoth.current.side) then
old_moves[i] = u.moves
u.moves = u.max_moves
end
end
-- Now set up an array containing the attack locations for each unit
local attacks = {}
-- We also need a numbered array of the possible attack hex coordinates
-- The order doesn't matter, as long as it is fixed
local reachable_hexes = {}
for i,u in ipairs(units) do
local locs = {} -- attack locations for this unit
for x, y in H.adjacent_tiles(enemy.x, enemy.y) do
local loc = {} -- attack location information for this unit for this hex
-- Make sure the hex is not occupied by unit that cannot move out of the way
if (not blocked_hexes:get(x, y) or ((x == u.x) and (y == u.y))) then
-- Check whether the unit can get to the hex
-- helper.distance_between() is much faster than wesnoth.find_path()
--> pre-filter using the former
local cost = H.distance_between(u.x, u.y, x, y)
-- If the distance is <= the unit's MP, then see if it can actually get there
-- This also means that only short paths have to be evaluated (in most situations)
if (cost <= u.moves) then
local path -- since cost is already defined outside this block
path, cost = wesnoth.find_path(u, x, y)
end
-- If the unit can get to this hex
if (cost <= u.moves) then
-- Store information about it in 'loc' and add this to 'locs'
-- Want coordinates (dst) and terrain defense (for sorting)
loc.dst = x * 1000 + y
loc.hit_prob = wesnoth.unit_defense(u, wesnoth.get_terrain(x, y))
table.insert(locs, loc)
-- Also mark this hex as usable
reachable_hexes[loc.dst] = true
end
end
end
-- Also add some top-level information for the unit
if locs[1] then
locs.src = u.x * 1000 + u.y -- The current position of the unit
locs.unit_i = i -- The position of the unit in the 'units' array
-- Now sort the possible attack locations for this unit by terrain defense
table.sort(locs, function(a, b) return a.hit_prob < b.hit_prob end)
-- Finally, add the attack locations for this unit to the 'attacks' array
table.insert(attacks, locs)
end
end
--DBG.dbms(attacks)
-- Reset moves for all units
for i,u in ipairs(units) do
if (u.side ~= wesnoth.current.side) then
u.moves = old_moves[i]
end
end
-- if the number of units that can attack is greater than cfg.skip_presort:
-- We also sort the attackers by their attack rating on their favorite hex
-- The motivation is that by starting with the strongest unit, we'll find the
-- best attack combo earlier, and it is more likely to find the best (or at
-- least a good combo) even when not all attack combinations are collected.
if (#attacks > cfg.skip_presort) then
for i,a in ipairs(attacks) do
local dst = a[1].dst
local x, y = math.floor(dst / 1000), dst % 1000
a.rating = battle_calcs.attack_rating(units[a.unit_i], enemy, { x, y })
end
table.sort(attacks, function(a,b) return a.rating > b.rating end)
--DBG.dbms(attacks)
end
-- To simplify and speed things up in the following, the field values
-- 'reachable_hexes' table needs to be consecutive integers
-- We also want a variable containing the number of elements in the array
-- (#reachable_hexes doesn't work because they keys are location indices)
local n_reach = 0
for k,hex in pairs(reachable_hexes) do
n_reach = n_reach + 1
reachable_hexes[k] = n_reach
end
--DBG.dbms(reachable_hexes)
-- If cfg.max_time is set, record the start time
-- For convenience, we store this in cfg
if cfg.max_time then
cfg.start_time = wesnoth.get_time_stamp() / 1000.
end
-- All this was just setting up the required information, now we call the
-- recursive function setting up the array of attackcombinations
local attack_combos = {} -- This will contain the return value
-- Temporary arrays (but need to be persistent across the recursion levels)
local combos_str, current_combo, hexes_used = {}, {}, {}
add_attack(attacks, reachable_hexes, n_reach, attack_combos, combos_str, current_combo, hexes_used, cfg)
--DBG.dbms(attack_combos)
-- Minor cleanup
cfg.start_time = nil
return attack_combos
end
return battle_calcs