mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-30 19:45:37 +00:00
battle_calcs library: add functions best_defense_map() and get_attack_combos_subset()
This commit is contained in:
parent
5fb4dd3816
commit
e7a71a51d7
@ -1317,4 +1317,309 @@ function battle_calcs.relative_damage_map(units, enemies, cache)
|
|||||||
return damage_map, own_damage_map, enemy_damage_map
|
return damage_map, own_damage_map, enemy_damage_map
|
||||||
end
|
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
|
return battle_calcs
|
||||||
|
Loading…
x
Reference in New Issue
Block a user