commit
81ef9ea390
60 changed files with 1138 additions and 1248 deletions
|
@ -1,2 +1,2 @@
|
|||
# wmllint: no translatables
|
||||
{ai/ais/ai_generic_rush.cfg}
|
||||
{ai/ais/ai_experimental.cfg}
|
||||
|
|
|
@ -125,7 +125,7 @@ function ai_helper.print_ts(...)
|
|||
local arg = { ... }
|
||||
arg[#arg+1] = string.format('[ t = %.3f ]', ts)
|
||||
|
||||
print(table.unpack(arg))
|
||||
std_print(table.unpack(arg))
|
||||
|
||||
return ts
|
||||
end
|
||||
|
@ -143,7 +143,7 @@ function ai_helper.print_ts_delta(start_time, ...)
|
|||
local arg = { ... }
|
||||
arg[#arg+1] = string.format('[ t = %.3f, dt = %.3f ]', ts, delta)
|
||||
|
||||
print(table.unpack(arg))
|
||||
std_print(table.unpack(arg))
|
||||
|
||||
return ts, delta
|
||||
end
|
||||
|
@ -639,6 +639,21 @@ function ai_helper.is_opposite_adjacent(hex1, hex2, center_hex)
|
|||
return false
|
||||
end
|
||||
|
||||
function ai_helper.get_locations_no_borders(location_filter)
|
||||
-- Returns the same locations array as wesnoth.get_locations(location_filter),
|
||||
-- but excluding hexes on the map border.
|
||||
--
|
||||
-- This is faster than alternative methods, at least with the current
|
||||
-- implementation of standard location filter evaluation by the engine.
|
||||
-- Note that this might not work if @location_filter is a vconfig object.
|
||||
|
||||
local old_include_borders = location_filter.include_borders
|
||||
location_filter.include_borders = false
|
||||
local locs = wesnoth.get_locations(location_filter)
|
||||
location_filter.include_borders = old_include_borders
|
||||
return locs
|
||||
end
|
||||
|
||||
function ai_helper.get_closest_location(hex, location_filter, unit)
|
||||
-- Get the location closest to @hex (in format { x, y })
|
||||
-- that matches @location_filter (in WML table format)
|
||||
|
@ -678,7 +693,7 @@ function ai_helper.get_closest_location(hex, location_filter, unit)
|
|||
|
||||
if unit then
|
||||
for _,loc in ipairs(locs) do
|
||||
local movecost = wesnoth.unit_movement_cost(unit, wesnoth.get_terrain(loc[1], loc[2]))
|
||||
local movecost = unit:movement(wesnoth.get_terrain(loc[1], loc[2]))
|
||||
if (movecost <= unit.max_moves) then return loc end
|
||||
end
|
||||
else
|
||||
|
@ -698,18 +713,13 @@ function ai_helper.get_passable_locations(location_filter, unit)
|
|||
-- excluding border hexes are returned
|
||||
|
||||
-- All hexes that are not on the map border
|
||||
local width, height = wesnoth.get_map_size()
|
||||
local all_locs = wesnoth.get_locations{
|
||||
x = '1-' .. width,
|
||||
y = '1-' .. height,
|
||||
{ "and", location_filter }
|
||||
}
|
||||
local all_locs = ai_helper.get_locations_no_borders(location_filter)
|
||||
|
||||
-- If @unit is provided, exclude terrain that's impassable for the unit
|
||||
if unit then
|
||||
local locs = {}
|
||||
for _,loc in ipairs(all_locs) do
|
||||
local movecost = wesnoth.unit_movement_cost(unit, wesnoth.get_terrain(loc[1], loc[2]))
|
||||
local movecost = unit:movement(wesnoth.get_terrain(loc[1], loc[2]))
|
||||
if (movecost <= unit.max_moves) then table.insert(locs, loc) end
|
||||
end
|
||||
return locs
|
||||
|
@ -901,12 +911,20 @@ end
|
|||
|
||||
function ai_helper.get_units_with_moves(filter)
|
||||
-- Note: the order of the filters and the [and] tags are important for speed reasons
|
||||
return wesnoth.get_units { { "and", { formula = "moves > 0" } }, { "and", filter } }
|
||||
return wesnoth.get_units {
|
||||
{ "and", { formula = "moves > 0" } },
|
||||
{ "not", { status = "petrified" } },
|
||||
{ "and", filter }
|
||||
}
|
||||
end
|
||||
|
||||
function ai_helper.get_units_with_attacks(filter)
|
||||
-- Note: the order of the filters and the [and] tags are important for speed reasons
|
||||
return wesnoth.get_units { { "and", { formula = "attacks_left > 0 and size(attacks) > 0" } }, { "and", filter } }
|
||||
return wesnoth.get_units {
|
||||
{ "and", { formula = "attacks_left > 0 and size(attacks) > 0" } },
|
||||
{ "not", { status = "petrified" } },
|
||||
{ "and", filter }
|
||||
}
|
||||
end
|
||||
|
||||
function ai_helper.get_visible_units(viewing_side, filter)
|
||||
|
@ -1052,7 +1070,7 @@ function ai_helper.get_closest_enemy(loc, side, cfg)
|
|||
x, y = loc[1], loc[2]
|
||||
end
|
||||
|
||||
local closest_distance, location = 9e99
|
||||
local closest_distance, location = math.huge
|
||||
for _,enemy in ipairs(enemies) do
|
||||
enemy_distance = M.distance_between(x, y, enemy.x, enemy.y)
|
||||
if (enemy_distance < closest_distance) then
|
||||
|
@ -1088,7 +1106,7 @@ function ai_helper.has_weapon_special(unit, special)
|
|||
end
|
||||
|
||||
function ai_helper.get_cheapest_recruit_cost()
|
||||
local cheapest_unit_cost = 9e99
|
||||
local cheapest_unit_cost = math.huge
|
||||
for _,recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
if wesnoth.unit_types[recruit_id].cost < cheapest_unit_cost then
|
||||
cheapest_unit_cost = wesnoth.unit_types[recruit_id].cost
|
||||
|
@ -1364,7 +1382,7 @@ function ai_helper.find_path_with_shroud(unit, x, y, cfg)
|
|||
if (u.side ~= viewing_side)
|
||||
and (not ai_helper.is_visible_unit(viewing_side, u))
|
||||
then
|
||||
wesnoth.extract_unit(u)
|
||||
u:extract()
|
||||
table.insert(extracted_units, u)
|
||||
end
|
||||
end
|
||||
|
@ -1376,7 +1394,7 @@ function ai_helper.find_path_with_shroud(unit, x, y, cfg)
|
|||
path, cost = wesnoth.find_path(unit, x, y, cfg_copy)
|
||||
|
||||
for _,extracted_unit in ipairs(extracted_units) do
|
||||
wesnoth.put_unit(extracted_unit)
|
||||
extracted_unit:to_map()
|
||||
end
|
||||
else
|
||||
path, cost = wesnoth.find_path(unit, x, y, cfg)
|
||||
|
@ -1408,7 +1426,7 @@ function ai_helper.find_best_move(units, rating_function, cfg)
|
|||
-- If this is an individual unit, turn it into an array
|
||||
if units.hitpoints then units = { units } end
|
||||
|
||||
local max_rating, best_hex, best_unit = -9e99, {}, {}
|
||||
local max_rating, best_hex, best_unit = - math.huge, {}, {}
|
||||
for _,unit in ipairs(units) do
|
||||
-- Hexes each unit can reach
|
||||
local reach_map = ai_helper.get_reachable_unocc(unit, cfg)
|
||||
|
@ -1452,7 +1470,7 @@ function ai_helper.move_unit_out_of_way(ai, unit, cfg)
|
|||
local reach = wesnoth.find_reach(unit, cfg)
|
||||
local reach_map = LS.create()
|
||||
|
||||
local max_rating, best_hex = -9e99
|
||||
local max_rating, best_hex = - math.huge
|
||||
for _,loc in ipairs(reach) do
|
||||
local unit_in_way = wesnoth.get_unit(loc[1], loc[2])
|
||||
if (not unit_in_way) -- also excludes current hex
|
||||
|
@ -1523,7 +1541,6 @@ function ai_helper.movefull_outofway_stopunit(ai, unit, x, y, cfg)
|
|||
if unit_in_way and (unit_in_way ~= unit)
|
||||
and ai_helper.is_visible_unit(viewing_side, unit_in_way)
|
||||
then
|
||||
--W.message { speaker = 'narrator', message = 'Moving out of way' }
|
||||
ai_helper.move_unit_out_of_way(ai, unit_in_way, cfg)
|
||||
end
|
||||
end
|
||||
|
@ -1665,7 +1682,7 @@ function ai_helper.get_attacks(units, cfg)
|
|||
for _,target in ipairs(attack_hex_map:get(loc[1], loc[2])) do
|
||||
local att_stats, def_stats
|
||||
if cfg.simulate_combat then
|
||||
local unit_dst = wesnoth.copy_unit(unit)
|
||||
local unit_dst = unit:clone()
|
||||
unit_dst.x, unit_dst.y = loc[1], loc[2]
|
||||
|
||||
local enemy = all_units[target.i]
|
||||
|
|
|
@ -38,45 +38,42 @@ function battle_calcs.unit_attack_info(unit, cache)
|
|||
resist_mod = {},
|
||||
alignment = unit_cfg.alignment
|
||||
}
|
||||
for attack in wml.child_range(unit_cfg, 'attack') do
|
||||
local attacks = unit.attacks
|
||||
for i_a = 1,#attacks do
|
||||
local attack = attacks[i_a]
|
||||
-- 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
|
||||
for _,sp in ipairs(attack.specials) do
|
||||
if (sp[1] == 'damage') then -- this is 'backstab'
|
||||
if (sp[2].id == 'backstab') then
|
||||
a.backstab = true
|
||||
else
|
||||
-- magical, marksman
|
||||
if (sp[1] == 'chance_to_hit') then
|
||||
a[sp[2].id] = true
|
||||
else
|
||||
a[sp[1]] = true
|
||||
end
|
||||
if (sp[2].id == 'charge') then a.charge = true end
|
||||
end
|
||||
else
|
||||
-- magical, marksman
|
||||
if (sp[1] == 'chance_to_hit') then
|
||||
a[sp[2].id or 'no_id'] = true
|
||||
else
|
||||
a[sp[1]] = true
|
||||
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
|
||||
|
||||
-- [attack]number= defaults to zero; must be defined for battle_calcs.best_weapons()
|
||||
a.number = a.number or 0
|
||||
a.damage = attack.damage
|
||||
a.type = attack.type
|
||||
a.range = attack.range
|
||||
-- number must be defined for battle_calcs.best_weapons()
|
||||
a.number = attack.number or 0
|
||||
|
||||
table.insert(unit_info.attacks, a)
|
||||
end
|
||||
|
||||
local attack_types = { "arcane", "blade", "cold", "fire", "impact", "pierce" }
|
||||
for _,attack_type in ipairs(attack_types) do
|
||||
unit_info.resist_mod[attack_type] = wesnoth.unit_resistance(unit, attack_type) / 100.
|
||||
unit_info.resist_mod[attack_type] = unit:resistance(attack_type) / 100.
|
||||
end
|
||||
|
||||
if cache then cache[cind] = unit_info end
|
||||
|
@ -202,10 +199,10 @@ function battle_calcs.best_weapons(attacker, defender, dst, cache)
|
|||
local defender_info = battle_calcs.unit_attack_info(defender, cache)
|
||||
|
||||
-- Best attacker weapon
|
||||
local max_rating, best_att_weapon, best_def_weapon = -9e99, 0, 0
|
||||
local max_rating, best_att_weapon, best_def_weapon = - math.huge, 0, 0
|
||||
for att_weapon_number,att_weapon in ipairs(attacker_info.attacks) do
|
||||
local att_damage = battle_calcs.strike_damage(attacker, defender, att_weapon_number, 0, { dst[1], dst[2] }, cache)
|
||||
local max_def_rating, tmp_best_def_weapon = -9e99, 0
|
||||
local max_def_rating, tmp_best_def_weapon = - math.huge, 0
|
||||
for def_weapon_number,def_weapon in ipairs(defender_info.attacks) do
|
||||
if (def_weapon.range == att_weapon.range) then
|
||||
local def_damage = battle_calcs.strike_damage(defender, attacker, def_weapon_number, 0, { defender.x, defender.y }, cache)
|
||||
|
@ -217,7 +214,7 @@ function battle_calcs.best_weapons(attacker, defender, dst, cache)
|
|||
end
|
||||
|
||||
local rating = att_damage * att_weapon.number
|
||||
if (max_def_rating > -9e99) then rating = rating - max_def_rating / 2. end
|
||||
if (max_def_rating > - math.huge) then rating = rating - max_def_rating / 2. end
|
||||
|
||||
if (rating > max_rating) then
|
||||
max_rating, best_att_weapon, best_def_weapon = rating, att_weapon_number, tmp_best_def_weapon
|
||||
|
@ -532,10 +529,10 @@ function battle_calcs.print_coefficients()
|
|||
dummy, coeffs = battle_calcs.battle_outcome_coefficients(cfg)
|
||||
end
|
||||
|
||||
print()
|
||||
print('Attacker: ' .. cfg.att.strikes .. ' strikes, can survive ' .. cfg.att.max_hits .. ' hits')
|
||||
print('Defender: ' .. cfg.def.strikes .. ' strikes, can survive ' .. cfg.def.max_hits .. ' hits')
|
||||
print('Chance of hits on defender: ')
|
||||
std_print()
|
||||
std_print('Attacker: ' .. cfg.att.strikes .. ' strikes, can survive ' .. cfg.att.max_hits .. ' hits')
|
||||
std_print('Defender: ' .. cfg.def.strikes .. ' strikes, can survive ' .. cfg.def.max_hits .. ' hits')
|
||||
std_print('Chance of hits on defender: ')
|
||||
|
||||
-- The first indices of coeffs are the possible number of hits the attacker can land on the defender
|
||||
for hits = 0,#coeffs do
|
||||
|
@ -570,8 +567,8 @@ function battle_calcs.print_coefficients()
|
|||
local skip_str = ''
|
||||
if combs.skip then skip_str = ' (skip)' end
|
||||
|
||||
print(hits .. skip_str .. ': ' .. str)
|
||||
print(' = ' .. hit_prob)
|
||||
std_print(hits .. skip_str .. ': ' .. str)
|
||||
std_print(' = ' .. hit_prob)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -680,8 +677,8 @@ function battle_calcs.battle_outcome(attacker, defender, cfg, cache)
|
|||
if (def_max_hits > att_strikes) then def_max_hits = att_strikes end
|
||||
|
||||
-- Probability of landing a hit
|
||||
local att_hit_prob = wesnoth.unit_defense(defender, wesnoth.get_terrain(defender.x, defender.y)) / 100.
|
||||
local def_hit_prob = wesnoth.unit_defense(attacker, wesnoth.get_terrain(dst[1], dst[2])) / 100.
|
||||
local att_hit_prob = defender:defense(wesnoth.get_terrain(defender.x, defender.y)) / 100.
|
||||
local def_hit_prob = attacker:defense(wesnoth.get_terrain(dst[1], dst[2])) / 100.
|
||||
|
||||
-- Magical: attack and defense, and under all circumstances
|
||||
if att_attack.magical then att_hit_prob = 0.7 end
|
||||
|
@ -731,7 +728,7 @@ function battle_calcs.simulate_combat_loc(attacker, dst, defender, weapon)
|
|||
-- when on terrain of same type as that at @dst, which is of form { x, y }
|
||||
-- If @weapon is set, use that weapon (Lua index starting at 1), otherwise use best weapon
|
||||
|
||||
local attacker_dst = wesnoth.copy_unit(attacker)
|
||||
local attacker_dst = attacker:clone()
|
||||
attacker_dst.x, attacker_dst.y = dst[1], dst[2]
|
||||
|
||||
if weapon then
|
||||
|
@ -837,7 +834,7 @@ function battle_calcs.attack_rating(attacker, defender, dst, cfg, cache)
|
|||
-- In addition, potentially leveling up in this attack is a huge bonus,
|
||||
-- proportional to the chance of it happening and the chance of not dying itself
|
||||
local level_bonus = 0.
|
||||
local defender_level = wesnoth.unit_types[defender.type].level
|
||||
local defender_level = defender.level
|
||||
if (attacker.max_experience - attacker.experience <= defender_level) then
|
||||
level_bonus = 1. - att_stats.hp_chance[0]
|
||||
else
|
||||
|
@ -849,7 +846,7 @@ function battle_calcs.attack_rating(attacker, defender, dst, cfg, cache)
|
|||
|
||||
|
||||
-- Now convert this into gold-equivalent value
|
||||
local attacker_value = wesnoth.unit_types[attacker.type].cost
|
||||
local attacker_value = attacker.cost
|
||||
|
||||
-- Being closer to leveling is good (this makes AI prefer units with lots of XP)
|
||||
local xp_bonus = attacker.experience / attacker.max_experience
|
||||
|
@ -886,7 +883,7 @@ function battle_calcs.attack_rating(attacker, defender, dst, cfg, cache)
|
|||
-- In addition, the defender potentially leveling up in this attack is a huge penalty,
|
||||
-- proportional to the chance of it happening and the chance of not dying itself
|
||||
local defender_level_penalty = 0.
|
||||
local attacker_level = wesnoth.unit_types[attacker.type].level
|
||||
local attacker_level = attacker.level
|
||||
if (defender.max_experience - defender.experience <= attacker_level) then
|
||||
defender_level_penalty = 1. - def_stats.hp_chance[0]
|
||||
else
|
||||
|
@ -897,7 +894,7 @@ function battle_calcs.attack_rating(attacker, defender, dst, cfg, cache)
|
|||
value_fraction = value_fraction - defender_level_penalty * defender_level_weight
|
||||
|
||||
-- Now convert this into gold-equivalent value
|
||||
local defender_value = wesnoth.unit_types[defender.type].cost
|
||||
local defender_value = defender.cost
|
||||
|
||||
-- If this is the enemy leader, make damage to it much more important
|
||||
if defender.canrecruit then
|
||||
|
@ -932,7 +929,7 @@ function battle_calcs.attack_rating(attacker, defender, dst, cfg, cache)
|
|||
-- We don't need a bonus for good terrain for the attacker, as that is covered in the damage calculation
|
||||
-- However, we add a small bonus for good terrain defense of the _defender_ on the _attack_ hex
|
||||
-- This is in order to take good terrain away from defender on next move, all else being equal
|
||||
local defender_defense = - wesnoth.unit_defense(defender, wesnoth.get_terrain(dst[1], dst[2])) / 100.
|
||||
local defender_defense = - defender:defense(wesnoth.get_terrain(dst[1], dst[2])) / 100.
|
||||
defender_value = defender_value + defender_defense * defense_weight
|
||||
|
||||
-- Get a very small bonus for hexes in between defender and AI leader
|
||||
|
@ -1011,15 +1008,13 @@ function battle_calcs.attack_combo_stats(tmp_attackers, tmp_dsts, defender, cach
|
|||
|
||||
--for hp,p in pairs(tmp_def_stats[i].hp_chance) do
|
||||
-- if (p > 0) then
|
||||
-- local dhp_norm = (hp - av) / defender.max_hitpoints * wesnoth.unit_types[defender.type].cost
|
||||
-- local dhp_norm = (hp - av) / defender.max_hitpoints * defender.cost
|
||||
-- local dvar = p * dhp_norm^2
|
||||
--print(hp,p,av, dvar)
|
||||
-- outcome_variance = outcome_variance + dvar
|
||||
-- n_outcomes = n_outcomes + 1
|
||||
-- end
|
||||
--end
|
||||
--outcome_variance = outcome_variance / n_outcomes
|
||||
--print('outcome_variance', outcome_variance)
|
||||
|
||||
-- Note that this is a variance, not a standard deviations (as in, it's squared),
|
||||
-- so it does not matter much for low-variance attacks, but takes on large values for
|
||||
|
@ -1030,7 +1025,7 @@ function battle_calcs.attack_combo_stats(tmp_attackers, tmp_dsts, defender, cach
|
|||
-- Almost, bonus should not be quite as high as a really high CTK
|
||||
-- This isn't quite true in reality, but can be refined later
|
||||
if AH.has_weapon_special(attacker, "slow") then
|
||||
rating = rating + wesnoth.unit_types[defender.type].cost / 2.
|
||||
rating = rating + defender.cost / 2.
|
||||
end
|
||||
|
||||
ratings[i] = { i, rating, base_rating, def_rating, att_rating }
|
||||
|
@ -1173,7 +1168,7 @@ function battle_calcs.get_attack_map_unit(unit, cfg)
|
|||
for _,unit in ipairs(all_units) do
|
||||
if (unit.moves > 0) then
|
||||
table.insert(units_MP, unit)
|
||||
wesnoth.extract_unit(unit)
|
||||
unit:extract()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1183,7 +1178,7 @@ function battle_calcs.get_attack_map_unit(unit, cfg)
|
|||
|
||||
-- Put the units back out there
|
||||
if (unit.side ~= wesnoth.current.side) then
|
||||
for _,uMP in ipairs(units_MP) do wesnoth.put_unit(uMP) end
|
||||
for _,uMP in ipairs(units_MP) do uMP:to_map() end
|
||||
end
|
||||
|
||||
for _,loc in ipairs(initial_reach) do
|
||||
|
@ -1250,7 +1245,7 @@ function battle_calcs.relative_damage_map(units, enemies, cache)
|
|||
-- against any of the enemy units
|
||||
local unit_ratings = {}
|
||||
for i,unit in ipairs(units) do
|
||||
local max_rating, best_enemy = -9e99, {}
|
||||
local max_rating, best_enemy = - math.huge, {}
|
||||
for _,enemy in ipairs(enemies) do
|
||||
local rating, defender_rating, attacker_rating =
|
||||
battle_calcs.attack_rating(unit, enemy, { unit.x, unit.y }, { enemy_leader_weight = 1 }, cache)
|
||||
|
@ -1267,7 +1262,7 @@ function battle_calcs.relative_damage_map(units, enemies, cache)
|
|||
-- Then we want the same thing for all of the enemy units (for the counter attack on enemy turn)
|
||||
local enemy_ratings = {}
|
||||
for i,enemy in ipairs(enemies) do
|
||||
local max_rating, best_unit = -9e99, {}
|
||||
local max_rating, best_unit = - math.huge, {}
|
||||
for _,unit in ipairs(units) do
|
||||
local rating, defender_rating, attacker_rating =
|
||||
battle_calcs.attack_rating(enemy, unit, { enemy.x, enemy.y }, { enemy_leader_weight = 1 }, cache)
|
||||
|
@ -1312,7 +1307,7 @@ function battle_calcs.best_defense_map(units, cfg)
|
|||
local defense_map = LS.create()
|
||||
|
||||
if cfg.ignore_these_units then
|
||||
for _,unit in ipairs(cfg.ignore_these_units) do wesnoth.extract_unit(unit) end
|
||||
for _,unit in ipairs(cfg.ignore_these_units) do unit:extract() end
|
||||
end
|
||||
|
||||
for _,unit in ipairs(units) do
|
||||
|
@ -1326,16 +1321,16 @@ function battle_calcs.best_defense_map(units, cfg)
|
|||
if max_moves then unit.moves = old_moves end
|
||||
|
||||
for _,loc in ipairs(reach) do
|
||||
local defense = 100 - wesnoth.unit_defense(unit, wesnoth.get_terrain(loc[1], loc[2]))
|
||||
local defense = 100 - unit:defense(wesnoth.get_terrain(loc[1], loc[2]))
|
||||
|
||||
if (defense > (defense_map:get(loc[1], loc[2]) or -9e99)) then
|
||||
if (defense > (defense_map:get(loc[1], loc[2]) or - math.huge)) then
|
||||
defense_map:insert(loc[1], loc[2], defense)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if cfg.ignore_these_units then
|
||||
for _,unit in ipairs(cfg.ignore_these_units) do wesnoth.put_unit(unit) end
|
||||
for _,unit in ipairs(cfg.ignore_these_units) do unit:to_map() end
|
||||
end
|
||||
|
||||
return defense_map
|
||||
|
@ -1378,7 +1373,7 @@ function battle_calcs.get_attack_combos_subset(units, enemy, cfg)
|
|||
|
||||
cfg = cfg or {}
|
||||
cfg.order_matters = cfg.order_matters or false
|
||||
cfg.max_combos = cfg.max_combos or 9e99
|
||||
cfg.max_combos = cfg.max_combos or math.huge
|
||||
cfg.max_time = cfg.max_time or false
|
||||
cfg.skip_presort = cfg.skip_presort or 5
|
||||
|
||||
|
@ -1534,7 +1529,7 @@ function battle_calcs.get_attack_combos_subset(units, enemy, cfg)
|
|||
-- Store information about it in 'loc' and add this to 'locs'
|
||||
-- Want coordinates (dst) and terrain defense (for sorting)
|
||||
loc.dst = xa * 1000 + ya
|
||||
loc.hit_prob = wesnoth.unit_defense(unit, wesnoth.get_terrain(xa, ya))
|
||||
loc.hit_prob = unit:defense(wesnoth.get_terrain(xa, ya))
|
||||
table.insert(locs, loc)
|
||||
|
||||
-- Also mark this hex as usable
|
||||
|
|
202
data/ai/lua/ca_castle_switch.lua
Normal file
202
data/ai/lua/ca_castle_switch.lua
Normal file
|
@ -0,0 +1,202 @@
|
|||
-------- Castle Switch CA --------------
|
||||
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local M = wesnoth.map
|
||||
|
||||
local CS_leader_score
|
||||
-- Note that leader_target is also needed by the recruiting CA, so it must be stored in 'data'
|
||||
|
||||
local function get_reachable_enemy_leaders(unit)
|
||||
-- We're cheating a little here and also find hidden enemy leaders. That's
|
||||
-- because a human player could make a pretty good educated guess as to where
|
||||
-- the enemy leaders are likely to be while the AI does not know how to do that.
|
||||
local potential_enemy_leaders = AH.get_live_units { canrecruit = 'yes',
|
||||
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
|
||||
}
|
||||
local enemy_leaders = {}
|
||||
for j,e in ipairs(potential_enemy_leaders) do
|
||||
local path, cost = wesnoth.find_path(unit, e.x, e.y, { ignore_units = true, viewing_side = 0 })
|
||||
if cost < AH.no_path then
|
||||
table.insert(enemy_leaders, e)
|
||||
end
|
||||
end
|
||||
|
||||
return enemy_leaders
|
||||
end
|
||||
|
||||
local ca_castle_switch = {}
|
||||
|
||||
function ca_castle_switch:evaluation(cfg, data)
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'castle_switch'
|
||||
if AH.print_eval() then AH.print_ts(' - Evaluating castle_switch CA:') end
|
||||
|
||||
if ai.aspects.passive_leader then
|
||||
-- Turn off this CA if the leader is passive
|
||||
return 0
|
||||
end
|
||||
|
||||
local leader = wesnoth.get_units {
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = 'yes',
|
||||
formula = '(movement_left = total_movement) and (hitpoints = max_hitpoints)'
|
||||
}[1]
|
||||
if not leader then
|
||||
-- CA is irrelevant if no leader or the leader may have moved from another CA
|
||||
data.leader_target = nil
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local cheapest_unit_cost = AH.get_cheapest_recruit_cost()
|
||||
|
||||
if data.leader_target and wesnoth.sides[wesnoth.current.side].gold >= cheapest_unit_cost then
|
||||
-- make sure move is still valid
|
||||
local next_hop = AH.next_hop(leader, data.leader_target[1], data.leader_target[2])
|
||||
if next_hop and next_hop[1] == data.leader_target[1]
|
||||
and next_hop[2] == data.leader_target[2] then
|
||||
return CS_leader_score
|
||||
end
|
||||
end
|
||||
|
||||
local keeps = AH.get_locations_no_borders {
|
||||
terrain = 'K*,K*^*,*^K*', -- Keeps
|
||||
{ "not", { {"filter", {}} }}, -- That have no unit
|
||||
{ "not", { radius = 6, {"filter", { canrecruit = 'yes',
|
||||
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
|
||||
}} }}, -- That are not too close to an enemy leader
|
||||
{ "not", {
|
||||
x = leader.x, y = leader.y, terrain = 'K*,K*^*,*^K*',
|
||||
radius = 3,
|
||||
{ "filter_radius", { terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*' } }
|
||||
}}, -- That are not close and connected to a keep the leader is on
|
||||
{ "filter_adjacent_location", {
|
||||
terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*'
|
||||
}} -- That are not one-hex keeps
|
||||
}
|
||||
if #keeps < 1 then
|
||||
-- Skip if there aren't extra keeps to evaluate
|
||||
-- In this situation we'd only switch keeps if we were running away
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local enemy_leaders = get_reachable_enemy_leaders(leader)
|
||||
|
||||
-- Look for the best keep
|
||||
local best_score, best_loc, best_turns = 0, {}, 3
|
||||
for i,loc in ipairs(keeps) do
|
||||
-- Only consider keeps within 2 turns movement
|
||||
local path, cost = wesnoth.find_path(leader, loc[1], loc[2])
|
||||
local score = 0
|
||||
-- Prefer closer keeps to enemy
|
||||
local turns = math.ceil(cost/leader.max_moves)
|
||||
if turns <= 2 then
|
||||
score = 1/turns
|
||||
for j,e in ipairs(enemy_leaders) do
|
||||
score = score + 1 / M.distance_between(loc[1], loc[2], e.x, e.y)
|
||||
end
|
||||
|
||||
if score > best_score then
|
||||
best_score = score
|
||||
best_loc = loc
|
||||
best_turns = turns
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If we're on a keep,
|
||||
-- don't move to another keep unless it's much better when uncaptured villages are present
|
||||
if best_score > 0 and wesnoth.get_terrain_info(wesnoth.get_terrain(leader.x, leader.y)).keep then
|
||||
local close_unowned_village = (wesnoth.get_villages {
|
||||
{ "and", {
|
||||
x = leader.x,
|
||||
y = leader.y,
|
||||
radius = leader.max_moves
|
||||
}},
|
||||
owner_side = 0
|
||||
})[1]
|
||||
if close_unowned_village then
|
||||
local score = 1/best_turns
|
||||
for j,e in ipairs(enemy_leaders) do
|
||||
-- count all distances as three less than they actually are
|
||||
score = score + 1 / (M.distance_between(leader.x, leader.y, e.x, e.y) - 3)
|
||||
end
|
||||
|
||||
if score > best_score then
|
||||
best_score = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if best_score > 0 then
|
||||
local next_hop = AH.next_hop(leader, best_loc[1], best_loc[2])
|
||||
|
||||
if next_hop and ((next_hop[1] ~= leader.x) or (next_hop[2] ~= leader.y)) then
|
||||
-- See if there is a nearby village that can be captured without delaying progress
|
||||
local close_villages = wesnoth.get_villages( {
|
||||
{ "and", { x = next_hop[1], y = next_hop[2], radius = leader.max_moves }},
|
||||
owner_side = 0 })
|
||||
for i,loc in ipairs(close_villages) do
|
||||
local path_village, cost_village = wesnoth.find_path(leader, loc[1], loc[2])
|
||||
if cost_village <= leader.moves then
|
||||
local dummy_leader = leader:clone()
|
||||
dummy_leader.x = loc[1]
|
||||
dummy_leader.y = loc[2]
|
||||
local path_keep, cost_keep = wesnoth.find_path(dummy_leader, best_loc[1], best_loc[2])
|
||||
local turns_from_keep = math.ceil(cost_keep/leader.max_moves)
|
||||
if turns_from_keep < best_turns
|
||||
or (turns_from_keep == 1 and wesnoth.sides[wesnoth.current.side].gold < cheapest_unit_cost)
|
||||
then
|
||||
-- There is, go there instead
|
||||
next_hop = loc
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
data.leader_target = next_hop
|
||||
|
||||
-- if we're on a keep, wait until there are no movable units on the castle before moving off
|
||||
CS_leader_score = 195000
|
||||
if wesnoth.get_terrain_info(wesnoth.get_terrain(leader.x, leader.y)).keep then
|
||||
local castle = AH.get_locations_no_borders {
|
||||
{ "and", {
|
||||
x = leader.x, y = leader.y, radius = 200,
|
||||
{ "filter_radius", { terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*' } }
|
||||
}}
|
||||
}
|
||||
local should_wait = false
|
||||
for i,loc in ipairs(castle) do
|
||||
local unit = wesnoth.get_unit(loc[1], loc[2])
|
||||
if (not AH.is_visible_unit(wesnoth.current.side, unit)) then
|
||||
should_wait = false
|
||||
break
|
||||
elseif unit.moves > 0 then
|
||||
should_wait = true
|
||||
end
|
||||
end
|
||||
if should_wait then
|
||||
CS_leader_score = 15000
|
||||
end
|
||||
end
|
||||
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return CS_leader_score
|
||||
end
|
||||
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
function ca_castle_switch:execution(cfg, data)
|
||||
local leader = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[1]
|
||||
|
||||
if AH.print_exec() then AH.print_ts(' Executing castle_switch CA') end
|
||||
if AH.show_messages() then wesnoth.wml_actions.message { speaker = leader.id, message = 'Switching castles' } end
|
||||
|
||||
AH.checked_move(ai, leader, data.leader_target[1], data.leader_target[2])
|
||||
data.leader_target = nil
|
||||
end
|
||||
|
||||
return ca_castle_switch
|
140
data/ai/lua/ca_grab_villages.lua
Normal file
140
data/ai/lua/ca_grab_villages.lua
Normal file
|
@ -0,0 +1,140 @@
|
|||
------- Grab Villages CA --------------
|
||||
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local BC = wesnoth.require "ai/lua/battle_calcs.lua"
|
||||
local M = wesnoth.map
|
||||
|
||||
local GV_unit, GV_village
|
||||
|
||||
local ca_grab_villages = {}
|
||||
|
||||
function ca_grab_villages:evaluation(cfg, data)
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'grab_villages'
|
||||
if AH.print_eval() then AH.print_ts(' - Evaluating grab_villages CA:') end
|
||||
|
||||
-- Check if there are units with moves left
|
||||
local units = AH.get_units_with_moves { side = wesnoth.current.side, canrecruit = 'no' }
|
||||
if (not units[1]) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local enemies = AH.get_attackable_enemies()
|
||||
|
||||
local villages = wesnoth.get_villages()
|
||||
-- Just in case:
|
||||
if (not villages[1]) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- First check if attacks are possible for any unit
|
||||
local return_value = 191000
|
||||
-- If one with > 50% chance of kill is possible, set return_value to lower than combat CA
|
||||
local attacks = ai.get_attacks()
|
||||
for i,a in ipairs(attacks) do
|
||||
if (#a.movements == 1) and (a.chance_to_kill > 0.5) then
|
||||
return_value = 90000
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Also find which locations can be attacked by enemies
|
||||
local enemy_attack_map = BC.get_attack_map(enemies).units
|
||||
|
||||
-- Now we go through the villages and units
|
||||
local max_rating, best_village, best_unit = - math.huge
|
||||
local village_ratings = {}
|
||||
for j,v in ipairs(villages) do
|
||||
-- First collect all information that only depends on the village
|
||||
local village_rating = 0 -- This is the unit independent rating
|
||||
|
||||
local unit_in_way = wesnoth.get_unit(v[1], v[2])
|
||||
|
||||
-- If an enemy can get within one move of the village, we want to hold it
|
||||
if enemy_attack_map:get(v[1], v[2]) then
|
||||
village_rating = village_rating + 100
|
||||
end
|
||||
|
||||
-- Unowned and enemy-owned villages get a large bonus
|
||||
local owner = wesnoth.get_village_owner(v[1], v[2])
|
||||
if (not owner) then
|
||||
village_rating = village_rating + 10000
|
||||
else
|
||||
if wesnoth.is_enemy(owner, wesnoth.current.side) then village_rating = village_rating + 20000 end
|
||||
end
|
||||
|
||||
local enemy_distance_from_village = AH.get_closest_enemy(v)
|
||||
|
||||
-- Now we go on to the unit-dependent rating
|
||||
local best_unit_rating = - math.huge
|
||||
local reachable = false
|
||||
for i,u in ipairs(units) do
|
||||
-- Skip villages that have units other than 'u' itself on them
|
||||
local village_occupied = false
|
||||
if AH.is_visible_unit(wesnoth.current.side, unit_in_way) and ((unit_in_way ~= u)) then
|
||||
village_occupied = true
|
||||
end
|
||||
|
||||
-- Rate all villages that can be reached and are unoccupied by other units
|
||||
if (not village_occupied) then
|
||||
-- Path finding is expensive, so we do a first cut simply by distance
|
||||
-- There is no way a unit can get to the village if the distance is greater than its moves
|
||||
local dist = M.distance_between(u.x, u.y, v[1], v[2])
|
||||
if (dist <= u.moves) then
|
||||
local path, cost = wesnoth.find_path(u, v[1], v[2])
|
||||
if (cost <= u.moves) then
|
||||
village_rating = village_rating - 1
|
||||
reachable = true
|
||||
local rating = 0
|
||||
|
||||
-- Prefer strong units if enemies can reach the village, injured units otherwise
|
||||
if enemy_attack_map:get(v[1], v[2]) then
|
||||
rating = rating + u.hitpoints
|
||||
else
|
||||
rating = rating + u.max_hitpoints - u.hitpoints
|
||||
end
|
||||
|
||||
-- Prefer not backtracking and moving more distant units to capture villages
|
||||
local enemy_distance_from_unit = AH.get_closest_enemy({u.x, u.y})
|
||||
rating = rating - (enemy_distance_from_village + enemy_distance_from_unit)/5
|
||||
|
||||
if (rating > best_unit_rating) then
|
||||
best_unit_rating, best_unit = rating, u
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
village_ratings[v] = {village_rating, best_unit, reachable}
|
||||
end
|
||||
for j,v in ipairs(villages) do
|
||||
local rating = village_ratings[v][1]
|
||||
if village_ratings[v][3] and rating > max_rating then
|
||||
max_rating, best_village, best_unit = rating, v, village_ratings[v][2]
|
||||
end
|
||||
end
|
||||
|
||||
if best_village then
|
||||
GV_unit, GV_village = best_unit, best_village
|
||||
if (max_rating >= 1000) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return return_value
|
||||
else
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
end
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
function ca_grab_villages:execution(cfg, data)
|
||||
if AH.print_exec() then AH.print_ts(' Executing grab_villages CA') end
|
||||
if AH.show_messages() then wesnoth.wml_actions.message { speaker = GV_unit.id, message = 'Grab villages' } end
|
||||
|
||||
AH.movefull_stopunit(ai, GV_unit, GV_village)
|
||||
GV_unit, GV_village = nil, nil
|
||||
end
|
||||
|
||||
return ca_grab_villages
|
|
@ -26,6 +26,8 @@ local M = wesnoth.map
|
|||
-- so that we can follow up with stronger units. In addition, use of poison or
|
||||
-- slow attacks is strongly discouraged. See code for exact equations.
|
||||
|
||||
local XP_attack
|
||||
|
||||
local ca_attack_highxp = {}
|
||||
|
||||
function ca_attack_highxp:evaluation(cfg, data)
|
||||
|
@ -82,7 +84,7 @@ function ca_attack_highxp:evaluation(cfg, data)
|
|||
if (not target_infos[1]) then return 0 end
|
||||
|
||||
-- The following location sets are used so that we at most need to call
|
||||
-- find_reach() and wesnoth.copy_unit() once per unit
|
||||
-- find_reach() and unit:clone() once per unit
|
||||
local reaches = LS.create()
|
||||
local attacker_copies = LS.create()
|
||||
|
||||
|
@ -217,7 +219,7 @@ function ca_attack_highxp:evaluation(cfg, data)
|
|||
if attacker_copies:get(attacker.x, attacker.y) then
|
||||
attacker_copy = attacker_copies:get(attacker.x, attacker.y)
|
||||
else
|
||||
attacker_copy = wesnoth.copy_unit(attacker)
|
||||
attacker_copy = attacker:clone()
|
||||
attacker_copies:insert(attacker.x, attacker.y, attacker_copy)
|
||||
end
|
||||
|
||||
|
@ -242,14 +244,14 @@ function ca_attack_highxp:evaluation(cfg, data)
|
|||
rating = 1000
|
||||
|
||||
local enemy_value_loss = (target.hitpoints - def_stats.average_hp) / target.max_hitpoints
|
||||
enemy_value_loss = enemy_value_loss * wesnoth.unit_types[target.type].cost
|
||||
enemy_value_loss = enemy_value_loss * target.cost
|
||||
|
||||
-- We want the _least_ damage to the enemy, so the minus sign is no typo!
|
||||
rating = rating - enemy_value_loss
|
||||
|
||||
local own_value_loss = (attacker_copy.hitpoints - att_stats.average_hp) / attacker_copy.max_hitpoints
|
||||
own_value_loss = own_value_loss + att_stats.hp_chance[0]
|
||||
own_value_loss = own_value_loss * wesnoth.unit_types[attacker_copy.type].cost
|
||||
own_value_loss = own_value_loss * attacker_copy.cost
|
||||
|
||||
rating = rating - own_value_loss
|
||||
|
||||
|
@ -284,15 +286,15 @@ function ca_attack_highxp:evaluation(cfg, data)
|
|||
end
|
||||
|
||||
if best_attack then
|
||||
data.XP_attack = best_attack
|
||||
XP_attack = best_attack
|
||||
end
|
||||
|
||||
return max_ca_score
|
||||
end
|
||||
|
||||
function ca_attack_highxp:execution(cfg, data)
|
||||
AH.robust_move_and_attack(ai, data.XP_attack.src, data.XP_attack.dst, data.XP_attack.target, { weapon = data.XP_attack.attack_num })
|
||||
data.XP_attack = nil
|
||||
AH.robust_move_and_attack(ai, XP_attack.src, XP_attack.dst, XP_attack.target, { weapon = XP_attack.attack_num })
|
||||
XP_attack = nil
|
||||
end
|
||||
|
||||
return ca_attack_highxp
|
||||
|
|
62
data/ai/lua/ca_move_to_any_enemy.lua
Normal file
62
data/ai/lua/ca_move_to_any_enemy.lua
Normal file
|
@ -0,0 +1,62 @@
|
|||
------- Move To Any Enemy CA --------------
|
||||
-- Move AI units toward any enemy on the map. This has a very low CA score and
|
||||
-- only kicks in when the AI would do nothing else. It prevents the AI from
|
||||
-- being inactive on maps without enemy leaders and villages.
|
||||
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
local MTAE_unit, MTAE_destination
|
||||
|
||||
local ca_move_to_any_enemy = {}
|
||||
|
||||
function ca_move_to_any_enemy:evaluation(cfg, data)
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'move_to_any_enemy'
|
||||
if AH.print_eval() then AH.print_ts(' - Evaluating move_to_any_enemy CA:') end
|
||||
|
||||
local units = AH.get_units_with_moves {
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = 'no'
|
||||
}
|
||||
|
||||
if (not units[1]) then
|
||||
-- No units with moves left
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local unit, destination
|
||||
-- Find a unit that has a path to an space close to an enemy
|
||||
for i,u in ipairs(units) do
|
||||
local distance, target = AH.get_closest_enemy({u.x, u.y})
|
||||
if target then
|
||||
unit = u
|
||||
|
||||
local x, y = wesnoth.find_vacant_tile(target.x, target.y)
|
||||
destination = AH.next_hop(unit, x, y)
|
||||
|
||||
if destination then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (not destination) then
|
||||
-- No path was found
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
MTAE_destination = destination
|
||||
MTAE_unit = unit
|
||||
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 1000
|
||||
end
|
||||
|
||||
function ca_move_to_any_enemy:execution(cfg, data)
|
||||
if AH.print_exec() then AH.print_ts(' Executing move_to_any_enemy CA') end
|
||||
AH.checked_move(ai, MTAE_unit, MTAE_destination[1], MTAE_destination[2])
|
||||
MTAE_unit, MTAE_destination = nil,nil
|
||||
end
|
||||
|
||||
return ca_move_to_any_enemy
|
25
data/ai/lua/ca_place_healers.lua
Normal file
25
data/ai/lua/ca_place_healers.lua
Normal file
|
@ -0,0 +1,25 @@
|
|||
------- Place Healers CA --------------
|
||||
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local HS = wesnoth.require "ai/micro_ais/cas/ca_healer_move.lua"
|
||||
|
||||
local ca_place_healers = {}
|
||||
|
||||
function ca_place_healers:evaluation(cfg, data)
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'place_healers'
|
||||
if AH.print_eval() then AH.print_ts(' - Evaluating place_healers CA:') end
|
||||
|
||||
if HS:evaluation(cfg, data) > 0 then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 96000
|
||||
end
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
function ca_place_healers:execution(cfg, data)
|
||||
if AH.print_exec(cfg, data) then AH.print_ts(' Executing place_healers CA') end
|
||||
HS:execution()
|
||||
end
|
||||
|
||||
return ca_place_healers
|
45
data/ai/lua/ca_recruit_rushers.lua
Normal file
45
data/ai/lua/ca_recruit_rushers.lua
Normal file
|
@ -0,0 +1,45 @@
|
|||
-- Make the generic_recruit_engine functions work as external CAs
|
||||
|
||||
local ca_castle_switch
|
||||
for ai_tag in wml.child_range(wesnoth.sides[wesnoth.current.side].__cfg, 'ai') do
|
||||
for stage in wml.child_range(ai_tag, 'stage') do
|
||||
for ca in wml.child_range(stage, 'candidate_action') do
|
||||
if ca.location and string.find(ca.location, 'ca_castle_switch') then
|
||||
ca_castle_switch = wesnoth.require("ai/lua/ca_castle_switch.lua")
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local dummy_engine = { data = {} }
|
||||
|
||||
local params = { score_function = (function() return 196000 end) }
|
||||
if ca_castle_switch then
|
||||
params.min_turn_1_recruit = (function() return ca_castle_switch:evaluation({}, dummy_engine.data) > 0 end)
|
||||
params.leader_takes_village = (function()
|
||||
if ca_castle_switch:evaluation({}, dummy_engine.data) > 0 then
|
||||
local take_village = #(wesnoth.get_villages {
|
||||
x = dummy_engine.data.leader_target[1],
|
||||
y = dummy_engine.data.leader_target[2]
|
||||
}) > 0
|
||||
return take_village
|
||||
end
|
||||
return not ai.aspects.passive_leader
|
||||
end
|
||||
)
|
||||
end
|
||||
|
||||
wesnoth.require("ai/lua/generic_recruit_engine.lua").init(dummy_engine, params)
|
||||
|
||||
local ca_recruit_rushers = {}
|
||||
|
||||
function ca_recruit_rushers:evaluation(cfg, data)
|
||||
return dummy_engine:recruit_rushers_eval()
|
||||
end
|
||||
|
||||
function ca_recruit_rushers:execution(cfg, data)
|
||||
return dummy_engine:recruit_rushers_exec()
|
||||
end
|
||||
|
||||
return ca_recruit_rushers
|
43
data/ai/lua/ca_retreat_injured.lua
Normal file
43
data/ai/lua/ca_retreat_injured.lua
Normal file
|
@ -0,0 +1,43 @@
|
|||
------- Retreat CA --------------
|
||||
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local R = wesnoth.require "ai/lua/retreat.lua"
|
||||
|
||||
local retreat_unit, retreat_loc
|
||||
|
||||
local ca_retreat_injured = {}
|
||||
|
||||
function ca_retreat_injured:evaluation(cfg, data)
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'retreat_injured'
|
||||
if AH.print_eval() then AH.print_ts(' - Evaluating retreat_injured CA:') end
|
||||
|
||||
local units = AH.get_units_with_moves { side = wesnoth.current.side }
|
||||
local unit, loc = R.retreat_injured_units(units)
|
||||
if unit then
|
||||
retreat_unit = unit
|
||||
retreat_loc = loc
|
||||
|
||||
-- First check if attacks are possible for any unit
|
||||
-- If one with > 50% chance of kill is possible, set return_value to lower than combat CA
|
||||
local attacks = ai.get_attacks()
|
||||
for i,a in ipairs(attacks) do
|
||||
if (#a.movements == 1) and (a.chance_to_kill > 0.5) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 95000
|
||||
end
|
||||
end
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 192000
|
||||
end
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
function ca_retreat_injured:execution(cfg, data)
|
||||
if AH.print_exec() then AH.print_ts(' Executing retreat_injured CA') end
|
||||
AH.robust_move_and_attack(ai, retreat_unit, retreat_loc)
|
||||
retreat_unit = nil
|
||||
retreat_loc = nil
|
||||
end
|
||||
|
||||
return ca_retreat_injured
|
103
data/ai/lua/ca_spread_poison.lua
Normal file
103
data/ai/lua/ca_spread_poison.lua
Normal file
|
@ -0,0 +1,103 @@
|
|||
------- Spread Poison CA --------------
|
||||
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
local SP_attack
|
||||
|
||||
local ca_spread_poison = {}
|
||||
|
||||
function ca_spread_poison:evaluation(cfg, data)
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'spread_poison'
|
||||
if AH.print_eval() then AH.print_ts(' - Evaluating spread_poison CA:') end
|
||||
|
||||
-- If a unit with a poisoned weapon can make an attack, we'll do that preferentially
|
||||
-- (with some exceptions)
|
||||
local poisoners = AH.get_units_with_attacks { side = wesnoth.current.side,
|
||||
{ "filter_wml", {
|
||||
{ "attack", {
|
||||
{ "specials", {
|
||||
{ "poison", { } }
|
||||
} }
|
||||
} }
|
||||
} },
|
||||
canrecruit = 'no'
|
||||
}
|
||||
if (not poisoners[1]) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local attacks = AH.get_attacks(poisoners)
|
||||
if (not attacks[1]) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Go through all possible attacks with poisoners
|
||||
local max_rating, best_attack = - math.huge
|
||||
for i,a in ipairs(attacks) do
|
||||
local attacker = wesnoth.get_unit(a.src.x, a.src.y)
|
||||
local defender = wesnoth.get_unit(a.target.x, a.target.y)
|
||||
|
||||
-- Don't try to poison a unit that cannot be poisoned
|
||||
local cant_poison = defender.status.poisoned or defender.status.unpoisonable
|
||||
|
||||
-- For now, we also simply don't poison units on villages (unless standard combat CA does it)
|
||||
local defender_terrain = wesnoth.get_terrain(defender.x, defender.y)
|
||||
local on_village = wesnoth.get_terrain_info(defender_terrain).village
|
||||
|
||||
-- Also, poisoning units that would level up through the attack or could level on their turn as a result is very bad
|
||||
local about_to_level = defender.max_experience - defender.experience <= (attacker.level * 2)
|
||||
|
||||
if (not cant_poison) and (not on_village) and (not about_to_level) then
|
||||
-- Strongest enemy gets poisoned first
|
||||
local rating = defender.hitpoints
|
||||
|
||||
-- Always attack enemy leader, if possible
|
||||
if defender.canrecruit then rating = rating + 1000 end
|
||||
|
||||
-- Enemies that can regenerate are not good targets
|
||||
if defender:ability('regenerate') then rating = rating - 1000 end
|
||||
|
||||
-- More priority to enemies on strong terrain
|
||||
local defender_defense = 100 - defender:defense(defender_terrain)
|
||||
rating = rating + defender_defense / 4.
|
||||
|
||||
-- For the same attacker/defender pair, go to strongest terrain
|
||||
local attacker_terrain = wesnoth.get_terrain(a.dst.x, a.dst.y)
|
||||
local attacker_defense = 100 - attacker:defense(attacker_terrain)
|
||||
rating = rating + attacker_defense / 2.
|
||||
|
||||
-- And from village everything else being equal
|
||||
local is_village = wesnoth.get_terrain_info(attacker_terrain).village
|
||||
if is_village then rating = rating + 0.5 end
|
||||
|
||||
if rating > max_rating then
|
||||
max_rating, best_attack = rating, a
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if best_attack then
|
||||
SP_attack = best_attack
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 190000
|
||||
end
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
function ca_spread_poison:execution(cfg, data)
|
||||
local attacker = wesnoth.get_unit(SP_attack.src.x, SP_attack.src.y)
|
||||
-- If several attacks have poison, this will always find the last one
|
||||
local is_poisoner, poison_weapon = AH.has_weapon_special(attacker, "poison")
|
||||
|
||||
if AH.print_exec() then AH.print_ts(' Executing spread_poison CA') end
|
||||
if AH.show_messages() then wesnoth.wml_actions.message { speaker = attacker.id, message = 'Poison attack' } end
|
||||
|
||||
AH.robust_move_and_attack(ai, attacker, SP_attack.dst, SP_attack.target, { weapon = poison_weapon })
|
||||
|
||||
SP_attack = nil
|
||||
end
|
||||
|
||||
return ca_spread_poison
|
74
data/ai/lua/ca_village_hunt.lua
Normal file
74
data/ai/lua/ca_village_hunt.lua
Normal file
|
@ -0,0 +1,74 @@
|
|||
------- Village Hunt CA --------------
|
||||
-- Give extra priority to seeking villages if we have less than our share
|
||||
-- our share is defined as being slightly more than the total/the number of sides
|
||||
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
local ca_village_hunt = {}
|
||||
|
||||
function ca_village_hunt:evaluation(cfg, data)
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'village_hunt'
|
||||
if AH.print_eval() then AH.print_ts(' - Evaluating village_hunt CA:') end
|
||||
|
||||
local villages = wesnoth.get_villages()
|
||||
|
||||
if not villages[1] then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local my_villages = wesnoth.get_villages { owner_side = wesnoth.current.side }
|
||||
|
||||
if #my_villages > #villages / #wesnoth.sides then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local allied_villages = wesnoth.get_villages { {"filter_owner", { {"ally_of", { side = wesnoth.current.side }} }} }
|
||||
if #allied_villages == #villages then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local units = AH.get_units_with_moves {
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = false
|
||||
}
|
||||
|
||||
if not units[1] then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 30000
|
||||
end
|
||||
|
||||
function ca_village_hunt:execution(cfg, data)
|
||||
local unit = AH.get_units_with_moves({
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = false
|
||||
})[1]
|
||||
|
||||
if AH.print_exec() then AH.print_ts(' Executing village_hunt CA') end
|
||||
|
||||
local villages = wesnoth.get_villages()
|
||||
local best_cost, target = AH.no_path
|
||||
for i,v in ipairs(villages) do
|
||||
if not wesnoth.match_location(v[1], v[2], { {"filter_owner", { {"ally_of", { side = wesnoth.current.side }} }} }) then
|
||||
local path, cost = wesnoth.find_path(unit, v[1], v[2], { ignore_units = true, max_cost = best_cost })
|
||||
if cost < best_cost then
|
||||
target = v
|
||||
best_cost = cost
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if target then
|
||||
local x, y = wesnoth.find_vacant_tile(target[1], target[2], unit)
|
||||
local dest = AH.next_hop(unit, x, y)
|
||||
AH.checked_move(ai, unit, dest[1], dest[2])
|
||||
end
|
||||
end
|
||||
|
||||
return ca_village_hunt
|
|
@ -13,7 +13,7 @@ return {
|
|||
-- min_turn_1_recruit: function that returns true if only enough units to grab nearby villages should be recruited turn 1, false otherwise
|
||||
-- (default always returns false)
|
||||
-- leader_takes_village: function that returns true if and only if the leader is going to move to capture a village this turn
|
||||
-- (default always returns true)
|
||||
-- (default returns 'not ai.aspects.passive_leader')
|
||||
-- Note: the recruiting code assumes full knowledge of units on the map and the recruit lists of other sides for the purpose of
|
||||
-- finding the best unit types to recruit. It does not work otherwise. It assumes normal vision of the AI side (that is, it disregards
|
||||
-- hidden enemy units) for determining from which keep hex the leader should recruit and on which castle hexes to recruit new units
|
||||
|
@ -24,17 +24,8 @@ return {
|
|||
math.randomseed(os.time())
|
||||
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local LS = wesnoth.require "location_set"
|
||||
local M = wesnoth.map
|
||||
|
||||
local function print_time(...)
|
||||
if turn_start_time then
|
||||
AH.print_ts_delta(turn_start_time, ...)
|
||||
else
|
||||
AH.print_ts(...)
|
||||
end
|
||||
end
|
||||
|
||||
local recruit_data = {}
|
||||
|
||||
local no_village_cost = function(recruit_id)
|
||||
|
@ -91,7 +82,7 @@ return {
|
|||
local best_defense = 100
|
||||
|
||||
for i, terrain in ipairs(terrain_archetypes) do
|
||||
local defense = wesnoth.unit_defense(unit, terrain)
|
||||
local defense = unit:defense(terrain)
|
||||
if defense < best_defense then
|
||||
best_defense = defense
|
||||
end
|
||||
|
@ -112,7 +103,7 @@ return {
|
|||
-- In several cases this function only approximates the correct value (eg Thunderguard vs Goblin Spearman has damage capped by target health)
|
||||
-- In some cases (like poison), this approximation is preferred to the actual value.
|
||||
local best_damage = 0
|
||||
local best_attack = nil
|
||||
local best_attack
|
||||
local best_poison_damage = 0
|
||||
-- Steadfast is currently disabled because it biases the AI too much in favour of Guardsmen
|
||||
-- Basically it sees the defender stats for damage and wrongfully concludes that the unit is amazing
|
||||
|
@ -178,13 +169,13 @@ return {
|
|||
-- TODO: find out what actual probability of getting to backstab is
|
||||
damage_multiplier = damage_multiplier*(special_multiplier*0.5 + 0.5)
|
||||
damage_bonus = damage_bonus+(special_bonus*0.5)
|
||||
if mod.value ~= nil then
|
||||
if mod.value then
|
||||
weapon_damage = (weapon_damage+mod.value)/2
|
||||
end
|
||||
else
|
||||
damage_multiplier = damage_multiplier*special_multiplier
|
||||
damage_bonus = damage_bonus+special_bonus
|
||||
if mod.value ~= nil then
|
||||
if mod.value then
|
||||
weapon_damage = mod.value
|
||||
end
|
||||
end
|
||||
|
@ -193,13 +184,15 @@ return {
|
|||
|
||||
-- Handle drain for defender
|
||||
local drain_recovery = 0
|
||||
for defender_attack in wml.child_range(defender.__cfg, 'attack') do
|
||||
local defender_attacks = defender.attacks
|
||||
for i_d = 1,#defender_attacks do
|
||||
local defender_attack = defender_attacks[i_d]
|
||||
if (defender_attack.range == attack.range) then
|
||||
for special in wml.child_range(defender_attack, 'specials') do
|
||||
if wml.get_child(special, 'drains') and drainable(attacker) then
|
||||
for _,sp in ipairs(defender_attack.specials) do
|
||||
if (sp[1] == 'drains') and drainable(attacker) then
|
||||
-- TODO: calculate chance to hit
|
||||
-- currently assumes 50% chance to hit using supplied constant
|
||||
local attacker_resistance = wesnoth.unit_resistance(attacker, defender_attack.type)
|
||||
local attacker_resistance = attacker:resistance(defender_attack.type)
|
||||
drain_recovery = (defender_attack.damage*defender_attack.number*attacker_resistance*attacker_defense/2)/10000
|
||||
end
|
||||
end
|
||||
|
@ -207,7 +200,7 @@ return {
|
|||
end
|
||||
|
||||
defense = defense/100.0
|
||||
local resistance = wesnoth.unit_resistance(defender, attack.type)
|
||||
local resistance = defender:resistance(attack.type)
|
||||
if steadfast and (resistance < 100) then
|
||||
resistance = 100 - ((100 - resistance) * 2)
|
||||
if (resistance < 50) then
|
||||
|
@ -260,8 +253,8 @@ return {
|
|||
name = "X",
|
||||
random_gender = false
|
||||
}
|
||||
local can_poison = poisonable(unit) and (not wesnoth.unit_ability(unit, 'regenerate'))
|
||||
local flat_defense = wesnoth.unit_defense(unit, "Gt")
|
||||
local can_poison = poisonable(unit) and (not unit:ability('regenerate'))
|
||||
local flat_defense = unit:defense("Gt")
|
||||
local best_defense = get_best_defense(unit)
|
||||
|
||||
local recruit = wesnoth.create_unit {
|
||||
|
@ -270,10 +263,10 @@ return {
|
|||
name = "X",
|
||||
random_gender = false
|
||||
}
|
||||
local recruit_flat_defense = wesnoth.unit_defense(recruit, "Gt")
|
||||
local recruit_flat_defense = recruit:defense("Gt")
|
||||
local recruit_best_defense = get_best_defense(recruit)
|
||||
|
||||
local can_poison_retaliation = poisonable(recruit) and (not wesnoth.unit_ability(recruit, 'regenerate'))
|
||||
local can_poison_retaliation = poisonable(recruit) and (not recruit:ability('regenerate'))
|
||||
best_flat_attack, best_flat_damage, flat_poison = get_best_attack(recruit, unit, flat_defense, recruit_best_defense, can_poison)
|
||||
best_high_defense_attack, best_high_defense_damage, high_defense_poison = get_best_attack(recruit, unit, best_defense, recruit_flat_defense, can_poison)
|
||||
best_retaliation, best_retaliation_damage, retaliation_poison = get_best_attack(unit, recruit, recruit_flat_defense, best_defense, can_poison_retaliation)
|
||||
|
@ -291,9 +284,10 @@ return {
|
|||
end
|
||||
|
||||
function can_slow(unit)
|
||||
for defender_attack in wml.child_range(unit.__cfg, 'attack') do
|
||||
for special in wml.child_range(defender_attack, 'specials') do
|
||||
if wml.get_child(special, 'slow') then
|
||||
local attacks = unit.attacks
|
||||
for i_a = 1,#attacks do
|
||||
for _,sp in ipairs(attacks[i_a].specials) do
|
||||
if (sp[1] == 'slow') then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
@ -372,12 +366,12 @@ return {
|
|||
end
|
||||
end
|
||||
|
||||
if data.recruit == nil then
|
||||
if not data.recruit then
|
||||
data.recruit = init_data(leader)
|
||||
end
|
||||
data.recruit.cheapest_unit_cost = cheapest_unit_cost
|
||||
|
||||
local score = 180000 -- default score if one not provided. Same as RCA AI
|
||||
local score = 180010 -- default score if one not provided, just above RCA AI recruiting
|
||||
if params.score_function then
|
||||
score = params.score_function()
|
||||
end
|
||||
|
@ -396,7 +390,7 @@ return {
|
|||
local possible_enemy_recruit_count = 0
|
||||
|
||||
local function add_unit_type(unit_type)
|
||||
if enemy_counts[unit_type] == nil then
|
||||
if not enemy_counts[unit_type] then
|
||||
table.insert(enemy_types, unit_type)
|
||||
enemy_counts[unit_type] = 1
|
||||
else
|
||||
|
@ -439,7 +433,7 @@ return {
|
|||
|
||||
function ai_cas:recruit_rushers_eval()
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'recruit_rushers'
|
||||
if AH.print_eval() then print_time(' - Evaluating recruit_rushers CA:') end
|
||||
if AH.print_eval() then AH.print_ts(' - Evaluating recruit_rushers CA:') end
|
||||
|
||||
local score = do_recruit_eval(recruit_data)
|
||||
if score == 0 then
|
||||
|
@ -452,6 +446,7 @@ return {
|
|||
end
|
||||
|
||||
function ai_cas:recruit_rushers_exec()
|
||||
if AH.print_exec() then AH.print_ts(' Executing recruit_rushers CA') end
|
||||
if AH.show_messages() then wesnoth.wml_actions.message { speaker = 'narrator', message = 'Recruiting' } end
|
||||
|
||||
local enemy_counts = recruit_data.recruit.enemy_counts
|
||||
|
@ -480,7 +475,7 @@ return {
|
|||
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
local analysis = analyze_enemy_unit(unit_type, recruit_id)
|
||||
|
||||
if recruit_effectiveness[recruit_id] == nil then
|
||||
if not recruit_effectiveness[recruit_id] then
|
||||
recruit_effectiveness[recruit_id] = {damage = 0, poison_damage = 0}
|
||||
recruit_vulnerability[recruit_id] = 0
|
||||
end
|
||||
|
@ -494,23 +489,23 @@ return {
|
|||
recruit_vulnerability[recruit_id] = recruit_vulnerability[recruit_id] + (analysis.retaliation.damage * enemy_counts[unit_type])^3
|
||||
|
||||
local attack_type = analysis.defense.attack.type
|
||||
if attack_type_count[attack_type] == nil then
|
||||
if not attack_type_count[attack_type] then
|
||||
attack_type_count[attack_type] = 0
|
||||
end
|
||||
attack_type_count[attack_type] = attack_type_count[attack_type] + recruit_count[recruit_id]
|
||||
|
||||
local attack_range = analysis.defense.attack.range
|
||||
if attack_range_count[attack_range] == nil then
|
||||
if not attack_range_count[attack_range] then
|
||||
attack_range_count[attack_range] = 0
|
||||
end
|
||||
attack_range_count[attack_range] = attack_range_count[attack_range] + recruit_count[recruit_id]
|
||||
|
||||
if unit_attack_type_count[recruit_id] == nil then
|
||||
if not unit_attack_type_count[recruit_id] then
|
||||
unit_attack_type_count[recruit_id] = {}
|
||||
end
|
||||
unit_attack_type_count[recruit_id][attack_type] = true
|
||||
|
||||
if unit_attack_range_count[recruit_id] == nil then
|
||||
if not unit_attack_range_count[recruit_id] then
|
||||
unit_attack_range_count[recruit_id] = {}
|
||||
end
|
||||
unit_attack_range_count[recruit_id][attack_range] = true
|
||||
|
@ -546,7 +541,7 @@ return {
|
|||
end
|
||||
end
|
||||
-- Correct count of units for each range
|
||||
local most_common_range = nil
|
||||
local most_common_range
|
||||
local most_common_range_count = 0
|
||||
for range, count in pairs(attack_range_count) do
|
||||
attack_range_count[range] = count/enemy_type_count
|
||||
|
@ -560,18 +555,18 @@ return {
|
|||
attack_type_count[attack_type] = count/enemy_type_count
|
||||
end
|
||||
|
||||
local recruit_type = nil
|
||||
local recruit_type
|
||||
local leader = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[1]
|
||||
repeat
|
||||
recruit_data.recruit.best_hex, recruit_data.recruit.target_hex = ai_cas:find_best_recruit_hex(leader, recruit_data)
|
||||
recruit_type = ai_cas:find_best_recruit(attack_type_count, unit_attack_type_count, recruit_effectiveness, recruit_vulnerability, attack_range_count, unit_attack_range_count, most_common_range_count)
|
||||
until recruit_type ~= nil
|
||||
until recruit_type
|
||||
|
||||
if wesnoth.unit_types[recruit_type].cost <= wesnoth.sides[wesnoth.current.side].gold then
|
||||
AH.checked_recruit(ai, recruit_type, recruit_data.recruit.best_hex[1], recruit_data.recruit.best_hex[2])
|
||||
|
||||
-- If the recruited unit cannot reach the target hex, return it to the pool of targets
|
||||
if recruit_data.recruit.target_hex ~= nil and recruit_data.recruit.target_hex[1] ~= nil then
|
||||
if recruit_data.recruit.target_hex and recruit_data.recruit.target_hex[1] then
|
||||
local unit = wesnoth.get_unit(recruit_data.recruit.best_hex[1], recruit_data.recruit.best_hex[2])
|
||||
local path, cost = wesnoth.find_path(unit, recruit_data.recruit.target_hex[1], recruit_data.recruit.target_hex[2], {viewing_side=0, max_cost=unit.max_moves+1})
|
||||
if cost > unit.max_moves then
|
||||
|
@ -595,12 +590,8 @@ return {
|
|||
|
||||
function get_current_castle(leader, data)
|
||||
if (not data.castle) or (data.castle.x ~= leader.x) or (data.castle.y ~= leader.y) then
|
||||
data.castle = {}
|
||||
local width,height,border = wesnoth.get_map_size()
|
||||
|
||||
data.castle = {
|
||||
locs = wesnoth.get_locations {
|
||||
x = "1-"..width, y = "1-"..height,
|
||||
locs = AH.get_locations_no_borders {
|
||||
{ "filter_vision", { side = wesnoth.current.side, visible = 'yes' } },
|
||||
{ "and", {
|
||||
x = leader.x, y = leader.y, radius = 200,
|
||||
|
@ -650,9 +641,9 @@ return {
|
|||
|
||||
if AH.print_eval() then
|
||||
if village[1] then
|
||||
print("Recruit at: " .. best_hex[1] .. "," .. best_hex[2] .. " -> " .. village[1] .. "," .. village[2])
|
||||
std_print("Recruit at: " .. best_hex[1] .. "," .. best_hex[2] .. " -> " .. village[1] .. "," .. village[2])
|
||||
else
|
||||
print("Recruit at: " .. best_hex[1] .. "," .. best_hex[2])
|
||||
std_print("Recruit at: " .. best_hex[1] .. "," .. best_hex[2])
|
||||
end
|
||||
end
|
||||
return best_hex, village
|
||||
|
@ -693,7 +684,6 @@ return {
|
|||
if recruit_data.castle.loose_gold_limit >= recruit_data.recruit.cheapest_unit_cost then
|
||||
gold_limit = recruit_data.castle.loose_gold_limit
|
||||
end
|
||||
--print (recruit_data.castle.loose_gold_limit .. " " .. recruit_data.recruit.cheapest_unit_cost .. " " .. gold_limit)
|
||||
|
||||
local recruitable_units = {}
|
||||
|
||||
|
@ -767,17 +757,17 @@ return {
|
|||
if can_slow(recruit_unit) then
|
||||
unit_score["slows"] = true
|
||||
end
|
||||
if wesnoth.match_unit(recruit_unit, { ability = "healing" }) then
|
||||
if recruit_unit:matches { ability = "healing" } then
|
||||
unit_score["heals"] = true
|
||||
end
|
||||
if wesnoth.match_unit(recruit_unit, { ability = "skirmisher" }) then
|
||||
if recruit_unit:matches { ability = "skirmisher" } then
|
||||
unit_score["skirmisher"] = true
|
||||
end
|
||||
recruitable_units[recruit_id] = recruit_unit
|
||||
end
|
||||
local healer_count, healable_count = get_unit_counts_for_healing()
|
||||
local best_score = 0
|
||||
local recruit_type = nil
|
||||
local recruit_type
|
||||
local offense_weight = 2.5
|
||||
local defense_weight = 1/hp_ratio^0.5
|
||||
local move_weight = math.max((distance_to_enemy/20)^2, 0.25)
|
||||
|
@ -815,7 +805,7 @@ return {
|
|||
local score = offense_score*offense_weight + defense_score*defense_weight + move_score*move_weight + bonus
|
||||
|
||||
if AH.print_eval() then
|
||||
print(recruit_id .. " score: " .. offense_score*offense_weight .. " + " .. defense_score*defense_weight .. " + " .. move_score*move_weight .. " + " .. bonus .. " = " .. score)
|
||||
std_print(recruit_id .. " score: " .. offense_score*offense_weight .. " + " .. defense_score*defense_weight .. " + " .. move_score*move_weight .. " + " .. bonus .. " = " .. score)
|
||||
end
|
||||
if score > best_score and wesnoth.unit_types[recruit_id].cost <= gold_limit then
|
||||
best_score = score
|
||||
|
@ -854,7 +844,7 @@ return {
|
|||
-- TODO get list of villages not owned by allies instead
|
||||
-- this may have false positives (villages that can't be reached due to difficult/impassible terrain)
|
||||
local exclude_x, exclude_y = "0", "0"
|
||||
if data.castle.assigned_villages_x ~= nil and data.castle.assigned_villages_x[1] then
|
||||
if data.castle.assigned_villages_x and data.castle.assigned_villages_x[1] then
|
||||
exclude_x = table.concat(data.castle.assigned_villages_x, ",")
|
||||
exclude_y = table.concat(data.castle.assigned_villages_y, ",")
|
||||
end
|
||||
|
@ -876,7 +866,7 @@ return {
|
|||
data.castle.assigned_villages_x = {}
|
||||
data.castle.assigned_villages_y = {}
|
||||
|
||||
if not params.leader_takes_village or params.leader_takes_village() then
|
||||
if not ai.aspects.passive_leader and (not params.leader_takes_village or params.leader_takes_village()) then
|
||||
-- skip one village for the leader
|
||||
for i,v in ipairs(villages) do
|
||||
local path, cost = wesnoth.find_path(leader, v[1], v[2], {max_cost = leader.max_moves+1})
|
||||
|
|
|
@ -1,607 +0,0 @@
|
|||
return {
|
||||
init = function(ai)
|
||||
|
||||
-- Grab a useful separate CA as a starting point
|
||||
local generic_rush = wesnoth.require("ai/lua/move_to_any_target.lua").init(ai)
|
||||
|
||||
-- More generic grunt rush (and can, in fact, be used with other unit types as well)
|
||||
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local BC = wesnoth.require "ai/lua/battle_calcs.lua"
|
||||
local LS = wesnoth.require "location_set"
|
||||
local HS = wesnoth.require "ai/micro_ais/cas/ca_healer_move.lua"
|
||||
local R = wesnoth.require "ai/lua/retreat.lua"
|
||||
local M = wesnoth.map
|
||||
|
||||
local function print_time(...)
|
||||
if generic_rush.data.turn_start_time then
|
||||
AH.print_ts_delta(generic_rush.data.turn_start_time, ...)
|
||||
else
|
||||
AH.print_ts(...)
|
||||
end
|
||||
end
|
||||
|
||||
------ Stats at beginning of turn -----------
|
||||
|
||||
-- This will be blacklisted after first execution each turn
|
||||
function generic_rush:stats_eval()
|
||||
local score = 999999
|
||||
return score
|
||||
end
|
||||
|
||||
function generic_rush:stats_exec()
|
||||
local tod = wesnoth.get_time_of_day()
|
||||
AH.print_ts(' Beginning of Turn ' .. wesnoth.current.turn .. ' (' .. tod.name ..') stats')
|
||||
generic_rush.data.turn_start_time = wesnoth.get_time_stamp() / 1000.
|
||||
|
||||
for i,s in ipairs(wesnoth.sides) do
|
||||
local total_hp = 0
|
||||
local units = AH.get_live_units { side = s.side }
|
||||
for i,u in ipairs(units) do total_hp = total_hp + u.hitpoints end
|
||||
local leader = wesnoth.get_units { side = s.side, canrecruit = 'yes' }[1]
|
||||
if leader then
|
||||
print(' Player ' .. s.side .. ' (' .. leader.type .. '): ' .. #units .. ' Units with total HP: ' .. total_hp)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
------- Recruit CA --------------
|
||||
|
||||
local params = {
|
||||
score_function = (function() return 300000 end),
|
||||
min_turn_1_recruit = (function() return generic_rush:castle_switch_eval() > 0 end),
|
||||
leader_takes_village = (function()
|
||||
if generic_rush:castle_switch_eval() > 0 then
|
||||
local take_village = #(wesnoth.get_villages {
|
||||
x = generic_rush.data.leader_target[1],
|
||||
y = generic_rush.data.leader_target[2]
|
||||
}) > 0
|
||||
return take_village
|
||||
end
|
||||
return true
|
||||
end
|
||||
)
|
||||
}
|
||||
wesnoth.require("ai/lua/generic_recruit_engine.lua").init(generic_rush, params)
|
||||
|
||||
-------- Castle Switch CA --------------
|
||||
local function get_reachable_enemy_leaders(unit)
|
||||
-- We're cheating a little here and also find hidden enemy leaders. That's
|
||||
-- because a human player could make a pretty good educated guess as to where
|
||||
-- the enemy leaders are likely to be while the AI does not know how to do that.
|
||||
local potential_enemy_leaders = AH.get_live_units { canrecruit = 'yes',
|
||||
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
|
||||
}
|
||||
local enemy_leaders = {}
|
||||
for j,e in ipairs(potential_enemy_leaders) do
|
||||
local path, cost = wesnoth.find_path(unit, e.x, e.y, { ignore_units = true, viewing_side = 0 })
|
||||
if cost < AH.no_path then
|
||||
table.insert(enemy_leaders, e)
|
||||
end
|
||||
end
|
||||
|
||||
return enemy_leaders
|
||||
end
|
||||
|
||||
function generic_rush:castle_switch_eval()
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'castle_switch'
|
||||
if AH.print_eval() then print_time(' - Evaluating castle_switch CA:') end
|
||||
|
||||
if ai.aspects.passive_leader then
|
||||
-- Turn off this CA if the leader is passive
|
||||
return 0
|
||||
end
|
||||
|
||||
local leader = wesnoth.get_units {
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = 'yes',
|
||||
formula = '(movement_left = total_movement) and (hitpoints = max_hitpoints)'
|
||||
}[1]
|
||||
if not leader then
|
||||
-- CA is irrelevant if no leader or the leader may have moved from another CA
|
||||
self.data.leader_target = nil
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local cheapest_unit_cost = AH.get_cheapest_recruit_cost()
|
||||
|
||||
if self.data.leader_target and wesnoth.sides[wesnoth.current.side].gold >= cheapest_unit_cost then
|
||||
-- make sure move is still valid
|
||||
local next_hop = AH.next_hop(leader, self.data.leader_target[1], self.data.leader_target[2])
|
||||
if next_hop and next_hop[1] == self.data.leader_target[1]
|
||||
and next_hop[2] == self.data.leader_target[2] then
|
||||
return self.data.leader_score
|
||||
end
|
||||
end
|
||||
|
||||
local width,height,border = wesnoth.get_map_size()
|
||||
local keeps = wesnoth.get_locations {
|
||||
terrain = 'K*,K*^*,*^K*', -- Keeps
|
||||
x = '1-'..width,
|
||||
y = '1-'..height,
|
||||
{ "not", { {"filter", {}} }}, -- That have no unit
|
||||
{ "not", { radius = 6, {"filter", { canrecruit = 'yes',
|
||||
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
|
||||
}} }}, -- That are not too close to an enemy leader
|
||||
{ "not", {
|
||||
x = leader.x, y = leader.y, terrain = 'K*,K*^*,*^K*',
|
||||
radius = 3,
|
||||
{ "filter_radius", { terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*' } }
|
||||
}}, -- That are not close and connected to a keep the leader is on
|
||||
{ "filter_adjacent_location", {
|
||||
terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*'
|
||||
}} -- That are not one-hex keeps
|
||||
}
|
||||
if #keeps < 1 then
|
||||
-- Skip if there aren't extra keeps to evaluate
|
||||
-- In this situation we'd only switch keeps if we were running away
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local enemy_leaders = get_reachable_enemy_leaders(leader)
|
||||
|
||||
-- Look for the best keep
|
||||
local best_score, best_loc, best_turns = 0, {}, 3
|
||||
for i,loc in ipairs(keeps) do
|
||||
-- Only consider keeps within 2 turns movement
|
||||
local path, cost = wesnoth.find_path(leader, loc[1], loc[2])
|
||||
local score = 0
|
||||
-- Prefer closer keeps to enemy
|
||||
local turns = math.ceil(cost/leader.max_moves)
|
||||
if turns <= 2 then
|
||||
score = 1/turns
|
||||
for j,e in ipairs(enemy_leaders) do
|
||||
score = score + 1 / M.distance_between(loc[1], loc[2], e.x, e.y)
|
||||
end
|
||||
|
||||
if score > best_score then
|
||||
best_score = score
|
||||
best_loc = loc
|
||||
best_turns = turns
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If we're on a keep,
|
||||
-- don't move to another keep unless it's much better when uncaptured villages are present
|
||||
if best_score > 0 and wesnoth.get_terrain_info(wesnoth.get_terrain(leader.x, leader.y)).keep then
|
||||
local close_unowned_village = (wesnoth.get_villages {
|
||||
{ "and", {
|
||||
x = leader.x,
|
||||
y = leader.y,
|
||||
radius = leader.max_moves
|
||||
}},
|
||||
owner_side = 0
|
||||
})[1]
|
||||
if close_unowned_village then
|
||||
local score = 1/best_turns
|
||||
for j,e in ipairs(enemy_leaders) do
|
||||
-- count all distances as three less than they actually are
|
||||
score = score + 1 / (M.distance_between(leader.x, leader.y, e.x, e.y) - 3)
|
||||
end
|
||||
|
||||
if score > best_score then
|
||||
best_score = 0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if best_score > 0 then
|
||||
local next_hop = AH.next_hop(leader, best_loc[1], best_loc[2])
|
||||
|
||||
if next_hop and ((next_hop[1] ~= leader.x) or (next_hop[2] ~= leader.y)) then
|
||||
-- See if there is a nearby village that can be captured without delaying progress
|
||||
local close_villages = wesnoth.get_villages( {
|
||||
{ "and", { x = next_hop[1], y = next_hop[2], radius = leader.max_moves }},
|
||||
owner_side = 0 })
|
||||
for i,loc in ipairs(close_villages) do
|
||||
local path_village, cost_village = wesnoth.find_path(leader, loc[1], loc[2])
|
||||
if cost_village <= leader.moves then
|
||||
local dummy_leader = wesnoth.copy_unit(leader)
|
||||
dummy_leader.x = loc[1]
|
||||
dummy_leader.y = loc[2]
|
||||
local path_keep, cost_keep = wesnoth.find_path(dummy_leader, best_loc[1], best_loc[2])
|
||||
local turns_from_keep = math.ceil(cost_keep/leader.max_moves)
|
||||
if turns_from_keep < best_turns
|
||||
or (turns_from_keep == 1 and wesnoth.sides[wesnoth.current.side].gold < cheapest_unit_cost)
|
||||
then
|
||||
-- There is, go there instead
|
||||
next_hop = loc
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
self.data.leader_target = next_hop
|
||||
|
||||
-- if we're on a keep, wait until there are no movable units on the castle before moving off
|
||||
self.data.leader_score = 290000
|
||||
if wesnoth.get_terrain_info(wesnoth.get_terrain(leader.x, leader.y)).keep then
|
||||
local castle = wesnoth.get_locations {
|
||||
x = "1-"..width, y = "1-"..height,
|
||||
{ "and", {
|
||||
x = leader.x, y = leader.y, radius = 200,
|
||||
{ "filter_radius", { terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*' } }
|
||||
}}
|
||||
}
|
||||
local should_wait = false
|
||||
for i,loc in ipairs(castle) do
|
||||
local unit = wesnoth.get_unit(loc[1], loc[2])
|
||||
if (not AH.is_visible_unit(wesnoth.current.side, unit)) then
|
||||
should_wait = false
|
||||
break
|
||||
elseif unit.moves > 0 then
|
||||
should_wait = true
|
||||
end
|
||||
end
|
||||
if should_wait then
|
||||
self.data.leader_score = 15000
|
||||
end
|
||||
end
|
||||
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return self.data.leader_score
|
||||
end
|
||||
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
function generic_rush:castle_switch_exec()
|
||||
local leader = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[1]
|
||||
|
||||
if AH.print_exec() then print_time(' Executing castle_switch CA') end
|
||||
if AH.show_messages() then wesnoth.wml_actions.message { speaker = leader.id, message = 'Switching castles' } end
|
||||
|
||||
AH.checked_move(ai, leader, self.data.leader_target[1], self.data.leader_target[2])
|
||||
self.data.leader_target = nil
|
||||
end
|
||||
|
||||
------- Grab Villages CA --------------
|
||||
|
||||
function generic_rush:grab_villages_eval()
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'grab_villages'
|
||||
if AH.print_eval() then print_time(' - Evaluating grab_villages CA:') end
|
||||
|
||||
-- Check if there are units with moves left
|
||||
local units = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'no',
|
||||
formula = 'movement_left > 0'
|
||||
}
|
||||
if (not units[1]) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local enemies = AH.get_attackable_enemies()
|
||||
|
||||
local villages = wesnoth.get_villages()
|
||||
-- Just in case:
|
||||
if (not villages[1]) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
--print('#units, #enemies', #units, #enemies)
|
||||
|
||||
-- First check if attacks are possible for any unit
|
||||
local return_value = 200000
|
||||
-- If one with > 50% chance of kill is possible, set return_value to lower than combat CA
|
||||
local attacks = ai.get_attacks()
|
||||
--print(#attacks)
|
||||
for i,a in ipairs(attacks) do
|
||||
if (#a.movements == 1) and (a.chance_to_kill > 0.5) then
|
||||
return_value = 90000
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Also find which locations can be attacked by enemies
|
||||
local enemy_attack_map = BC.get_attack_map(enemies).units
|
||||
|
||||
-- Now we go through the villages and units
|
||||
local max_rating, best_village, best_unit = -9e99, {}, {}
|
||||
local village_ratings = {}
|
||||
for j,v in ipairs(villages) do
|
||||
-- First collect all information that only depends on the village
|
||||
local village_rating = 0 -- This is the unit independent rating
|
||||
|
||||
local unit_in_way = wesnoth.get_unit(v[1], v[2])
|
||||
|
||||
-- If an enemy can get within one move of the village, we want to hold it
|
||||
if enemy_attack_map:get(v[1], v[2]) then
|
||||
--print(' within enemy reach', v[1], v[2])
|
||||
village_rating = village_rating + 100
|
||||
end
|
||||
|
||||
-- Unowned and enemy-owned villages get a large bonus
|
||||
local owner = wesnoth.get_village_owner(v[1], v[2])
|
||||
if (not owner) then
|
||||
village_rating = village_rating + 10000
|
||||
else
|
||||
if wesnoth.is_enemy(owner, wesnoth.current.side) then village_rating = village_rating + 20000 end
|
||||
end
|
||||
|
||||
local enemy_distance_from_village = AH.get_closest_enemy(v)
|
||||
|
||||
-- Now we go on to the unit-dependent rating
|
||||
local best_unit_rating = -9e99
|
||||
local reachable = false
|
||||
for i,u in ipairs(units) do
|
||||
-- Skip villages that have units other than 'u' itself on them
|
||||
local village_occupied = false
|
||||
if AH.is_visible_unit(wesnoth.current.side, unit_in_way) and ((unit_in_way ~= u)) then
|
||||
village_occupied = true
|
||||
end
|
||||
|
||||
-- Rate all villages that can be reached and are unoccupied by other units
|
||||
if (not village_occupied) then
|
||||
-- Path finding is expensive, so we do a first cut simply by distance
|
||||
-- There is no way a unit can get to the village if the distance is greater than its moves
|
||||
local dist = M.distance_between(u.x, u.y, v[1], v[2])
|
||||
if (dist <= u.moves) then
|
||||
local path, cost = wesnoth.find_path(u, v[1], v[2])
|
||||
if (cost <= u.moves) then
|
||||
village_rating = village_rating - 1
|
||||
reachable = true
|
||||
--print('Can reach:', u.id, v[1], v[2], cost)
|
||||
local rating = 0
|
||||
|
||||
-- Prefer strong units if enemies can reach the village, injured units otherwise
|
||||
if enemy_attack_map:get(v[1], v[2]) then
|
||||
rating = rating + u.hitpoints
|
||||
else
|
||||
rating = rating + u.max_hitpoints - u.hitpoints
|
||||
end
|
||||
|
||||
-- Prefer not backtracking and moving more distant units to capture villages
|
||||
local enemy_distance_from_unit = AH.get_closest_enemy({u.x, u.y})
|
||||
rating = rating - (enemy_distance_from_village + enemy_distance_from_unit)/5
|
||||
|
||||
if (rating > best_unit_rating) then
|
||||
best_unit_rating, best_unit = rating, u
|
||||
end
|
||||
--print(' rating:', rating)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
village_ratings[v] = {village_rating, best_unit, reachable}
|
||||
end
|
||||
for j,v in ipairs(villages) do
|
||||
local rating = village_ratings[v][1]
|
||||
if village_ratings[v][3] and rating > max_rating then
|
||||
max_rating, best_village, best_unit = rating, v, village_ratings[v][2]
|
||||
end
|
||||
end
|
||||
--print('max_rating', max_rating)
|
||||
|
||||
if (max_rating > -9e99) then
|
||||
self.data.unit, self.data.village = best_unit, best_village
|
||||
if (max_rating >= 1000) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return return_value
|
||||
else
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
end
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
function generic_rush:grab_villages_exec()
|
||||
if AH.print_exec() then print_time(' Executing grab_villages CA') end
|
||||
if AH.show_messages() then wesnoth.wml_actions.message { speaker = self.data.unit.id, message = 'Grab villages' } end
|
||||
|
||||
AH.movefull_stopunit(ai, self.data.unit, self.data.village)
|
||||
self.data.unit, self.data.village = nil, nil
|
||||
end
|
||||
|
||||
------- Spread Poison CA --------------
|
||||
|
||||
function generic_rush:spread_poison_eval()
|
||||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'spread_poison'
|
||||
if AH.print_eval() then print_time(' - Evaluating spread_poison CA:') end
|
||||
|
||||
-- If a unit with a poisoned weapon can make an attack, we'll do that preferentially
|
||||
-- (with some exceptions)
|
||||
local poisoners = AH.get_live_units { side = wesnoth.current.side,
|
||||
formula = 'attacks_left > 0',
|
||||
{ "filter_wml", {
|
||||
{ "attack", {
|
||||
{ "specials", {
|
||||
{ "poison", { } }
|
||||
} }
|
||||
} }
|
||||
} },
|
||||
canrecruit = 'no'
|
||||
}
|
||||
--print('#poisoners', #poisoners)
|
||||
if (not poisoners[1]) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
local attacks = AH.get_attacks(poisoners)
|
||||
--print('#attacks', #attacks)
|
||||
if (not attacks[1]) then
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Go through all possible attacks with poisoners
|
||||
local max_rating, best_attack = -9e99, {}
|
||||
for i,a in ipairs(attacks) do
|
||||
local attacker = wesnoth.get_unit(a.src.x, a.src.y)
|
||||
local defender = wesnoth.get_unit(a.target.x, a.target.y)
|
||||
|
||||
-- Don't try to poison a unit that cannot be poisoned
|
||||
local cant_poison = defender.status.poisoned or defender.status.unpoisonable
|
||||
|
||||
-- For now, we also simply don't poison units on villages (unless standard combat CA does it)
|
||||
local on_village = wesnoth.get_terrain_info(wesnoth.get_terrain(defender.x, defender.y)).village
|
||||
|
||||
-- Also, poisoning units that would level up through the attack or could level on their turn as a result is very bad
|
||||
local about_to_level = defender.max_experience - defender.experience <= (wesnoth.unit_types[attacker.type].level * 2)
|
||||
|
||||
if (not cant_poison) and (not on_village) and (not about_to_level) then
|
||||
-- Strongest enemy gets poisoned first
|
||||
local rating = defender.hitpoints
|
||||
|
||||
-- Always attack enemy leader, if possible
|
||||
if defender.canrecruit then rating = rating + 1000 end
|
||||
|
||||
-- Enemies that can regenerate are not good targets
|
||||
if wesnoth.unit_ability(defender, 'regenerate') then rating = rating - 1000 end
|
||||
|
||||
-- More priority to enemies on strong terrain
|
||||
local defender_defense = 100 - wesnoth.unit_defense(defender, wesnoth.get_terrain(defender.x, defender.y))
|
||||
rating = rating + defender_defense / 4.
|
||||
|
||||
-- For the same attacker/defender pair, go to strongest terrain
|
||||
local attack_defense = 100 - wesnoth.unit_defense(attacker, wesnoth.get_terrain(a.dst.x, a.dst.y))
|
||||
rating = rating + attack_defense / 2.
|
||||
--print('rating', rating)
|
||||
|
||||
-- And from village everything else being equal
|
||||
local is_village = wesnoth.get_terrain_info(wesnoth.get_terrain(a.dst.x, a.dst.y)).village
|
||||
if is_village then rating = rating + 0.5 end
|
||||
|
||||
if rating > max_rating then
|
||||
max_rating, best_attack = rating, a
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (max_rating > -9e99) then
|
||||
self.data.attack = best_attack
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 190000
|
||||
end
|
||||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
||||
return 0
|
||||
end
|
||||
|
||||
function generic_rush:spread_poison_exec()
|
||||
local attacker = wesnoth.get_unit(self.data.attack.src.x, self.data.attack.src.y)
|
||||
-- If several attacks have poison, this will always find the last one
|
||||
local is_poisoner, poison_weapon = AH.has_weapon_special(attacker, "poison")
|
||||
|
||||
if AH.print_exec() then print_time(' Executing spread_poison CA') end
|
||||
if AH.show_messages() then wesnoth.wml_actions.message { speaker = attacker.id, message = 'Poison attack' } end
|
||||
|
||||
AH.robust_move_and_attack(ai, attacker, self.data.attack.dst, self.data.attack.target, { weapon = poison_weapon })
|
||||
|
||||
self.data.attack = nil
|
||||
end
|
||||
|
||||
------- Place Healers CA --------------
|
||||
|
||||
function generic_rush:place_healers_eval()
|
||||
if HS:evaluation(ai, {}, self) > 0 then
|
||||
return 95000
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
function generic_rush:place_healers_exec()
|
||||
HS:execution(ai, nil, self)
|
||||
end
|
||||
|
||||
------- Retreat CA --------------
|
||||
|
||||
function generic_rush:retreat_injured_units_eval()
|
||||
local units = wesnoth.get_units {
|
||||
side = wesnoth.current.side,
|
||||
formula = 'movement_left > 0'
|
||||
}
|
||||
local unit, loc = R.retreat_injured_units(units)
|
||||
if unit then
|
||||
self.data.retreat_unit = unit
|
||||
self.data.retreat_loc = loc
|
||||
|
||||
-- First check if attacks are possible for any unit
|
||||
-- If one with > 50% chance of kill is possible, set return_value to lower than combat CA
|
||||
local attacks = ai.get_attacks()
|
||||
for i,a in ipairs(attacks) do
|
||||
if (#a.movements == 1) and (a.chance_to_kill > 0.5) then
|
||||
return 95000
|
||||
end
|
||||
end
|
||||
return 205000
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
function generic_rush:retreat_injured_units_exec()
|
||||
AH.robust_move_and_attack(ai, self.data.retreat_unit, self.data.retreat_loc)
|
||||
self.data.retreat_unit = nil
|
||||
self.data.retreat_loc = nil
|
||||
end
|
||||
|
||||
------- Village Hunt CA --------------
|
||||
-- Give extra priority to seeking villages if we have less than our share
|
||||
-- our share is defined as being slightly more than the total/the number of sides
|
||||
|
||||
function generic_rush:village_hunt_eval()
|
||||
local villages = wesnoth.get_villages()
|
||||
|
||||
if not villages[1] then
|
||||
return 0
|
||||
end
|
||||
|
||||
local my_villages = wesnoth.get_villages { owner_side = wesnoth.current.side }
|
||||
|
||||
if #my_villages > #villages / #wesnoth.sides then
|
||||
return 0
|
||||
end
|
||||
|
||||
local allied_villages = wesnoth.get_villages { {"filter_owner", { {"ally_of", { side = wesnoth.current.side }} }} }
|
||||
if #allied_villages == #villages then
|
||||
return 0
|
||||
end
|
||||
|
||||
local units = wesnoth.get_units {
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = false,
|
||||
formula = 'movement_left > 0'
|
||||
}
|
||||
|
||||
if not units[1] then
|
||||
return 0
|
||||
end
|
||||
|
||||
return 30000
|
||||
end
|
||||
|
||||
function generic_rush:village_hunt_exec()
|
||||
local unit = wesnoth.get_units({
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = false,
|
||||
formula = 'movement_left > 0'
|
||||
})[1]
|
||||
|
||||
local villages = wesnoth.get_villages()
|
||||
local target, best_cost = nil, AH.no_path
|
||||
for i,v in ipairs(villages) do
|
||||
if not wesnoth.match_location(v[1], v[2], { {"filter_owner", { {"ally_of", { side = wesnoth.current.side }} }} }) then
|
||||
local path, cost = wesnoth.find_path(unit, v[1], v[2], { ignore_units = true, max_cost = best_cost })
|
||||
if cost < best_cost then
|
||||
target = v
|
||||
best_cost = cost
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if target then
|
||||
local x, y = wesnoth.find_vacant_tile(target[1], target[2], unit)
|
||||
local dest = AH.next_hop(unit, x, y)
|
||||
AH.checked_move(ai, unit, dest[1], dest[2])
|
||||
end
|
||||
end
|
||||
|
||||
return generic_rush
|
||||
end
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
return {
|
||||
init = function(ai)
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
local move_to_any_target = {}
|
||||
|
||||
function move_to_any_target:move_to_enemy_eval()
|
||||
local units = wesnoth.get_units {
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = 'no',
|
||||
formula = 'movement_left > 0'
|
||||
}
|
||||
|
||||
if (not units[1]) then
|
||||
-- No units with moves left
|
||||
return 0
|
||||
end
|
||||
|
||||
local unit, destination
|
||||
-- Find a unit that has a path to an space close to an enemy
|
||||
for i,u in ipairs(units) do
|
||||
local distance, target = AH.get_closest_enemy({u.x, u.y})
|
||||
if target then
|
||||
unit = u
|
||||
|
||||
local x, y = wesnoth.find_vacant_tile(target.x, target.y)
|
||||
destination = AH.next_hop(unit, x, y)
|
||||
|
||||
if destination then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (not destination) then
|
||||
-- No path was found
|
||||
return 0
|
||||
end
|
||||
|
||||
self.data.destination = destination
|
||||
self.data.unit = unit
|
||||
|
||||
return 1
|
||||
end
|
||||
|
||||
function move_to_any_target:move_to_enemy_exec()
|
||||
AH.checked_move(ai, self.data.unit, self.data.destination[1], self.data.destination[2])
|
||||
end
|
||||
|
||||
return move_to_any_target
|
||||
end
|
||||
}
|
|
@ -12,7 +12,7 @@ local retreat_functions = {}
|
|||
function retreat_functions.min_hp(unit)
|
||||
-- The minimum hp to retreat is a function of level and terrain defense
|
||||
-- We want to stay longer on good terrain and leave early on very bad terrain
|
||||
local hp_per_level = wesnoth.unit_defense(unit, wesnoth.get_terrain(unit.x, unit.y))/15
|
||||
local hp_per_level = unit:defense(wesnoth.get_terrain(unit.x, unit.y))/15
|
||||
local level = unit.level
|
||||
|
||||
-- Leaders are considered to be higher level because of their value
|
||||
|
@ -38,7 +38,7 @@ function retreat_functions.retreat_injured_units(units)
|
|||
local regen, non_regen = {}, {}
|
||||
for i,u in ipairs(units) do
|
||||
if u.hitpoints < retreat_functions.min_hp(u) then
|
||||
if wesnoth.unit_ability(u, 'regenerate') then
|
||||
if u:ability('regenerate') then
|
||||
table.insert(regen, u)
|
||||
else
|
||||
table.insert(non_regen, u)
|
||||
|
@ -114,7 +114,7 @@ function retreat_functions.get_retreat_injured_units(healees, regenerates)
|
|||
|
||||
local healing_locs = retreat_functions.get_healing_locations()
|
||||
|
||||
local max_rating, best_loc, best_unit = -9e99, nil, nil
|
||||
local max_rating, best_loc, best_unit = - math.huge
|
||||
for i,u in ipairs(healees) do
|
||||
local possible_locations = wesnoth.find_reach(u)
|
||||
-- TODO: avoid ally's villages (may be preferable to lower rating so they will
|
||||
|
@ -176,7 +176,7 @@ function retreat_functions.get_retreat_injured_units(healees, regenerates)
|
|||
rating = rating - enemy_count * 100000
|
||||
|
||||
-- Penalty based on terrain defense for unit
|
||||
rating = rating - wesnoth.unit_defense(u, wesnoth.get_terrain(loc[1], loc[2]))/10
|
||||
rating = rating - u:defense(wesnoth.get_terrain(loc[1], loc[2]))/10
|
||||
|
||||
if (loc[1] == u.x) and (loc[2] == u.y) and (not u.status.poisoned) then
|
||||
if enemy_count == 0 then
|
||||
|
|
|
@ -25,7 +25,7 @@ local function custom_cost(x, y, unit, enemy_rating_map, prefer_map)
|
|||
-- must return values >=1 for the a* search to work.
|
||||
|
||||
local terrain = wesnoth.get_terrain(x, y)
|
||||
local move_cost = wesnoth.unit_movement_cost(unit, terrain)
|
||||
local move_cost = unit:movement(terrain)
|
||||
|
||||
move_cost = move_cost + (enemy_rating_map:get(x, y) or 0)
|
||||
|
||||
|
@ -66,7 +66,7 @@ function ca_assassin_move:execution(cfg)
|
|||
if (not enemy.status.petrified) then
|
||||
-- Need to "move" enemy next to unit for attack calculation
|
||||
-- Do this with a unit copy, so that no actual unit has to be moved
|
||||
local enemy_copy = wesnoth.copy_unit(enemy)
|
||||
local enemy_copy = enemy:clone()
|
||||
|
||||
-- First get the reach of the enemy with full moves though
|
||||
enemy_copy.moves = enemy_copy.max_moves
|
||||
|
@ -95,7 +95,7 @@ function ca_assassin_move:execution(cfg)
|
|||
-- Penalties for damage by enemies
|
||||
local enemy_rating_map = LS.create()
|
||||
enemy_damage_map:iter(function(x, y, enemy_damage)
|
||||
local hit_chance = (wesnoth.unit_defense(unit, wesnoth.get_terrain(x, y))) / 100.
|
||||
local hit_chance = (unit:defense(wesnoth.get_terrain(x, y))) / 100.
|
||||
|
||||
local rating = hit_chance * enemy_damage
|
||||
rating = rating / unit.max_hitpoints
|
||||
|
@ -105,7 +105,7 @@ function ca_assassin_move:execution(cfg)
|
|||
end)
|
||||
|
||||
-- Penalties for blocked hexes and ZOC
|
||||
local is_skirmisher = wesnoth.unit_ability(unit, "skirmisher")
|
||||
local is_skirmisher = unit:ability("skirmisher")
|
||||
for _,enemy in ipairs(enemies) do
|
||||
-- Hexes an enemy is on get a very large penalty
|
||||
enemy_rating_map:insert(enemy.x, enemy.y, (enemy_rating_map:get(enemy.x, enemy.y) or 0) + 100)
|
||||
|
|
|
@ -58,7 +58,7 @@ function ca_big_animals:execution(cfg)
|
|||
end)
|
||||
|
||||
-- Now find the one of these hexes that is closest to the goal
|
||||
local max_rating, best_hex = -9e99
|
||||
local max_rating, best_hex = - math.huge
|
||||
reach_map:iter( function(x, y, v)
|
||||
local rating = -wesnoth.map.distance_between(x, y, goal.goal_x, goal.goal_y)
|
||||
|
||||
|
@ -95,7 +95,7 @@ function ca_big_animals:execution(cfg)
|
|||
end
|
||||
|
||||
-- Finally, if the unit ended up next to enemies, attack the weakest of those
|
||||
local min_hp, target = 9e99
|
||||
local min_hp, target = math.huge
|
||||
for xa,ya in H.adjacent_tiles(unit.x, unit.y) do
|
||||
local enemy = wesnoth.get_unit(xa, ya)
|
||||
if AH.is_attackable_enemy(enemy) then
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
local BD_attacker, BD_target, BD_weapon, BD_bottleneck_attacks_done
|
||||
|
||||
local ca_bottleneck_attack = {}
|
||||
|
||||
function ca_bottleneck_attack:evaluation(cfg, data)
|
||||
|
@ -11,7 +13,7 @@ function ca_bottleneck_attack:evaluation(cfg, data)
|
|||
}
|
||||
if (not attackers[1]) then return 0 end
|
||||
|
||||
local max_rating, best_attacker, best_target, best_weapon = -9e99
|
||||
local max_rating, best_attacker, best_target, best_weapon = - math.huge
|
||||
for _,attacker in ipairs(attackers) do
|
||||
local targets = AH.get_attackable_enemies { { "filter_adjacent", { id = attacker.id } } }
|
||||
|
||||
|
@ -48,29 +50,29 @@ function ca_bottleneck_attack:evaluation(cfg, data)
|
|||
|
||||
if (not best_attacker) then
|
||||
-- In this case we take attacks away from all units
|
||||
data.BD_bottleneck_attacks_done = true
|
||||
BD_bottleneck_attacks_done = true
|
||||
else
|
||||
data.BD_bottleneck_attacks_done = false
|
||||
data.BD_attacker = best_attacker
|
||||
data.BD_target = best_target
|
||||
data.BD_weapon = best_weapon
|
||||
BD_bottleneck_attacks_done = false
|
||||
BD_attacker = best_attacker
|
||||
BD_target = best_target
|
||||
BD_weapon = best_weapon
|
||||
end
|
||||
|
||||
return cfg.ca_score
|
||||
end
|
||||
|
||||
function ca_bottleneck_attack:execution(cfg, data)
|
||||
if data.BD_bottleneck_attacks_done then
|
||||
if BD_bottleneck_attacks_done then
|
||||
local units = AH.get_units_with_attacks { side = wesnoth.current.side }
|
||||
for _,unit in ipairs(units) do
|
||||
AH.checked_stopunit_attacks(ai, unit)
|
||||
end
|
||||
else
|
||||
AH.checked_attack(ai, data.BD_attacker, data.BD_target, data.BD_weapon)
|
||||
AH.checked_attack(ai, BD_attacker, BD_target, BD_weapon)
|
||||
end
|
||||
|
||||
data.BD_attacker, data.BD_target, data.BD_weapon = nil, nil, nil
|
||||
data.BD_bottleneck_attacks_done = nil
|
||||
BD_attacker, BD_target, BD_weapon = nil, nil, nil
|
||||
BD_bottleneck_attacks_done = nil
|
||||
end
|
||||
|
||||
return ca_bottleneck_attack
|
||||
|
|
|
@ -5,6 +5,10 @@ local BC = wesnoth.require "ai/lua/battle_calcs.lua"
|
|||
local MAISD = wesnoth.require "ai/micro_ais/micro_ai_self_data.lua"
|
||||
local M = wesnoth.map
|
||||
|
||||
local BD_unit, BD_hex
|
||||
local BD_level_up_defender, BD_level_up_weapon, BD_bottleneck_moves_done
|
||||
local BD_is_my_territory, BD_def_map, BD_healer_map, BD_leadership_map, BD_healing_map
|
||||
|
||||
local function bottleneck_is_my_territory(map, enemy_map)
|
||||
-- Create map that contains 'true' for all hexes that are
|
||||
-- on the AI's side of the map
|
||||
|
@ -14,7 +18,7 @@ local function bottleneck_is_my_territory(map, enemy_map)
|
|||
-- If there is no leader, use first unit found
|
||||
local unit = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[1]
|
||||
if (not unit) then unit = wesnoth.get_units { side = wesnoth.current.side }[1] end
|
||||
local dummy_unit = wesnoth.copy_unit(unit)
|
||||
local dummy_unit = unit:clone()
|
||||
|
||||
local territory_map = LS.create()
|
||||
local width, height = wesnoth.get_map_size()
|
||||
|
@ -25,7 +29,7 @@ local function bottleneck_is_my_territory(map, enemy_map)
|
|||
dummy_unit.x, dummy_unit.y = x, y
|
||||
|
||||
-- Find lowest movement cost to own front-line hexes
|
||||
local min_cost, best_path = 9e99
|
||||
local min_cost, best_path = math.huge
|
||||
map:iter(function(xm, ym, v)
|
||||
local path, cost = AH.find_path_with_shroud(dummy_unit, xm, ym, { ignore_units = true })
|
||||
if (cost < min_cost) then
|
||||
|
@ -34,7 +38,7 @@ local function bottleneck_is_my_territory(map, enemy_map)
|
|||
end)
|
||||
|
||||
-- And the same to the enemy front line
|
||||
local min_cost_enemy, best_path_enemy = 9e99
|
||||
local min_cost_enemy, best_path_enemy = math.huge
|
||||
enemy_map:iter(function(xm, ym, v)
|
||||
local path, cost = AH.find_path_with_shroud(dummy_unit, xm, ym, { ignore_units = true })
|
||||
if (cost < min_cost_enemy) then
|
||||
|
@ -85,16 +89,16 @@ end
|
|||
local function bottleneck_create_positioning_map(max_value, data)
|
||||
-- Create the positioning maps for the healers and leaders, if not given by WML keys
|
||||
-- @max_value: the rating value for the first hex in the set
|
||||
-- data.BD_def_map must have been created when this function is called.
|
||||
-- BD_def_map must have been created when this function is called.
|
||||
|
||||
-- Find all locations adjacent to def_map.
|
||||
-- This might include hexes on the line itself.
|
||||
-- Only store those that are not in enemy territory.
|
||||
local map = LS.create()
|
||||
data.BD_def_map:iter(function(x, y, v)
|
||||
BD_def_map:iter(function(x, y, v)
|
||||
for xa,ya in H.adjacent_tiles(x, y) do
|
||||
if data.BD_is_my_territory:get(xa, ya) then
|
||||
local rating = data.BD_def_map:get(x, y) or 0
|
||||
if BD_is_my_territory:get(xa, ya) then
|
||||
local rating = BD_def_map:get(x, y) or 0
|
||||
rating = rating + (map:get(xa, ya) or 0)
|
||||
map:insert(xa, ya, rating)
|
||||
end
|
||||
|
@ -109,7 +113,7 @@ local function bottleneck_create_positioning_map(max_value, data)
|
|||
|
||||
-- We merge the defense map into this, as healers/leaders (by default)
|
||||
-- can take position on the front line
|
||||
map:union_merge(data.BD_def_map,
|
||||
map:union_merge(BD_def_map,
|
||||
function(x, y, v1, v2) return v1 or v2 end
|
||||
)
|
||||
|
||||
|
@ -126,18 +130,18 @@ local function bottleneck_get_rating(unit, x, y, has_leadership, is_healer, data
|
|||
-- Defense positioning rating
|
||||
-- We exclude healers/leaders here, as we don't necessarily want them on the front line
|
||||
if (not is_healer) and (not has_leadership) then
|
||||
rating = data.BD_def_map:get(x, y) or 0
|
||||
rating = BD_def_map:get(x, y) or 0
|
||||
end
|
||||
|
||||
-- Healer positioning rating
|
||||
if is_healer then
|
||||
local healer_rating = data.BD_healer_map:get(x, y) or 0
|
||||
local healer_rating = BD_healer_map:get(x, y) or 0
|
||||
if (healer_rating > rating) then rating = healer_rating end
|
||||
end
|
||||
|
||||
-- Leadership unit positioning rating
|
||||
if has_leadership then
|
||||
local leadership_rating = data.BD_leadership_map:get(x, y) or 0
|
||||
local leadership_rating = BD_leadership_map:get(x, y) or 0
|
||||
|
||||
-- If leadership unit is injured -> prefer hexes next to healers
|
||||
if (unit.hitpoints < unit.max_hitpoints) then
|
||||
|
@ -155,18 +159,18 @@ local function bottleneck_get_rating(unit, x, y, has_leadership, is_healer, data
|
|||
|
||||
-- Injured unit positioning
|
||||
if (unit.hitpoints < unit.max_hitpoints) then
|
||||
local healing_rating = data.BD_healing_map:get(x, y) or 0
|
||||
local healing_rating = BD_healing_map:get(x, y) or 0
|
||||
if (healing_rating > rating) then rating = healing_rating end
|
||||
end
|
||||
|
||||
-- If this did not produce a positive rating, we add a
|
||||
-- distance-based rating, to get units to the bottleneck in the first place
|
||||
if (rating <= 0) and data.BD_is_my_territory:get(x, y) then
|
||||
if (rating <= 0) and BD_is_my_territory:get(x, y) then
|
||||
local combined_dist = 0
|
||||
data.BD_def_map:iter(function(x_def, y_def, v)
|
||||
BD_def_map:iter(function(x_def, y_def, v)
|
||||
combined_dist = combined_dist + M.distance_between(x, y, x_def, y_def)
|
||||
end)
|
||||
combined_dist = combined_dist / data.BD_def_map:size()
|
||||
combined_dist = combined_dist / BD_def_map:size()
|
||||
rating = 1000 - combined_dist * 10.
|
||||
end
|
||||
|
||||
|
@ -192,9 +196,9 @@ local function bottleneck_move_out_of_way(unit_in_way, data)
|
|||
occ_hexes:insert(unit.x, unit.y)
|
||||
end
|
||||
|
||||
local best_reach, best_hex = -9e99
|
||||
local best_reach, best_hex = - math.huge
|
||||
for _,loc in ipairs(reach) do
|
||||
if data.BD_is_my_territory:get(loc[1], loc[2]) and (not occ_hexes:get(loc[1], loc[2])) then
|
||||
if BD_is_my_territory:get(loc[1], loc[2]) and (not occ_hexes:get(loc[1], loc[2])) then
|
||||
-- Criterion: MP left after the move has been done
|
||||
if (loc[3] > best_reach) then
|
||||
best_reach, best_hex = loc[3], { loc[1], loc[2] }
|
||||
|
@ -232,54 +236,54 @@ function ca_bottleneck_move:evaluation(cfg, data)
|
|||
if (not units[1]) then return 0 end
|
||||
|
||||
-- Set up the array that tells the AI where to defend the bottleneck
|
||||
data.BD_def_map = bottleneck_triple_from_keys(cfg.x, cfg.y, 10000)
|
||||
BD_def_map = bottleneck_triple_from_keys(cfg.x, cfg.y, 10000)
|
||||
|
||||
-- Territory map, describing which hex is on AI's side of the bottleneck
|
||||
-- This one is a bit expensive, esp. on large maps -> don't delete every move and reuse
|
||||
-- However, after a reload, data.BD_is_my_territory is an empty string
|
||||
-- -> need to recalculate in that case also (the reason is that is_my_territory is not a WML table)
|
||||
if (not data.BD_is_my_territory) or (type(data.BD_is_my_territory) == 'string') then
|
||||
-- However, after a reload, BD_is_my_territory is empty
|
||||
-- -> need to recalculate in that case also
|
||||
if (not BD_is_my_territory) or (type(BD_is_my_territory) == 'string') then
|
||||
local enemy_map = bottleneck_triple_from_keys(cfg.enemy_x, cfg.enemy_y, 10000)
|
||||
data.BD_is_my_territory = bottleneck_is_my_territory(data.BD_def_map, enemy_map)
|
||||
BD_is_my_territory = bottleneck_is_my_territory(BD_def_map, enemy_map)
|
||||
end
|
||||
|
||||
-- Healer positioning map
|
||||
if cfg.healer_x and cfg.healer_y then
|
||||
data.BD_healer_map = bottleneck_triple_from_keys(cfg.healer_x, cfg.healer_y, 5000)
|
||||
BD_healer_map = bottleneck_triple_from_keys(cfg.healer_x, cfg.healer_y, 5000)
|
||||
else
|
||||
data.BD_healer_map = bottleneck_create_positioning_map(5000, data)
|
||||
BD_healer_map = bottleneck_create_positioning_map(5000, data)
|
||||
end
|
||||
-- Use def_map values for any healer hexes that are defined in def_map as well
|
||||
data.BD_healer_map:inter_merge(data.BD_def_map,
|
||||
BD_healer_map:inter_merge(BD_def_map,
|
||||
function(x, y, v1, v2) return v2 or v1 end
|
||||
)
|
||||
|
||||
-- Leadership position map
|
||||
if cfg.leadership_x and cfg.leadership_y then
|
||||
data.BD_leadership_map = bottleneck_triple_from_keys(cfg.leadership_x, cfg.leadership_y, 4000)
|
||||
BD_leadership_map = bottleneck_triple_from_keys(cfg.leadership_x, cfg.leadership_y, 4000)
|
||||
else
|
||||
data.BD_leadership_map = bottleneck_create_positioning_map(4000, data)
|
||||
BD_leadership_map = bottleneck_create_positioning_map(4000, data)
|
||||
end
|
||||
-- Use def_map values for any leadership hexes that are defined in def_map as well
|
||||
data.BD_leadership_map:inter_merge(data.BD_def_map,
|
||||
BD_leadership_map:inter_merge(BD_def_map,
|
||||
function(x, y, v1, v2) return v2 or v1 end
|
||||
)
|
||||
|
||||
-- Healing map: positions next to healers
|
||||
-- Healers get moved with higher priority, so don't need to check their MP
|
||||
local healers = wesnoth.get_units { side = wesnoth.current.side, ability = "healing" }
|
||||
data.BD_healing_map = LS.create()
|
||||
BD_healing_map = LS.create()
|
||||
for _,healer in ipairs(healers) do
|
||||
for xa,ya in H.adjacent_tiles(healer.x, healer.y) do
|
||||
-- Cannot be on the line, and needs to be in own territory
|
||||
if data.BD_is_my_territory:get(xa, ya) then
|
||||
local min_dist = 9e99
|
||||
data.BD_def_map:iter( function(xd, yd, vd)
|
||||
if BD_is_my_territory:get(xa, ya) then
|
||||
local min_dist = math.huge
|
||||
BD_def_map:iter( function(xd, yd, vd)
|
||||
local dist_line = M.distance_between(xa, ya, xd, yd)
|
||||
if (dist_line < min_dist) then min_dist = dist_line end
|
||||
end)
|
||||
if (min_dist > 0) then
|
||||
data.BD_healing_map:insert(xa, ya, 3000 + min_dist) -- Farther away from enemy is good
|
||||
BD_healing_map:insert(xa, ya, 3000 + min_dist) -- Farther away from enemy is good
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -314,7 +318,7 @@ function ca_bottleneck_move:evaluation(cfg, data)
|
|||
local attacks = {}
|
||||
for _,enemy in ipairs(enemies) do
|
||||
for xa,ya in H.adjacent_tiles(enemy.x, enemy.y) do
|
||||
if data.BD_is_my_territory:get(xa, ya) then
|
||||
if BD_is_my_territory:get(xa, ya) then
|
||||
local unit_in_way = wesnoth.get_unit(xa, ya)
|
||||
if (not AH.is_visible_unit(wesnoth.current.side, unit_in_way)) then
|
||||
unit_in_way = nil
|
||||
|
@ -417,8 +421,8 @@ function ca_bottleneck_move:evaluation(cfg, data)
|
|||
|
||||
if (level_up_rating > max_rating) then
|
||||
max_rating, best_unit, best_hex = level_up_rating, unit, { loc[1], loc[2] }
|
||||
data.BD_level_up_defender = attack.defender
|
||||
data.BD_level_up_weapon = n_weapon
|
||||
BD_level_up_defender = attack.defender
|
||||
BD_level_up_weapon = n_weapon
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -428,7 +432,7 @@ function ca_bottleneck_move:evaluation(cfg, data)
|
|||
|
||||
-- Set the variables for the exec() function
|
||||
if (not best_hex) then
|
||||
data.BD_bottleneck_moves_done = true
|
||||
BD_bottleneck_moves_done = true
|
||||
else
|
||||
-- If there's another unit in the best location, moving it out of the way becomes the best move
|
||||
local unit_in_way = wesnoth.get_units { x = best_hex[1], y = best_hex[2],
|
||||
|
@ -441,19 +445,19 @@ function ca_bottleneck_move:evaluation(cfg, data)
|
|||
if unit_in_way then
|
||||
best_hex = bottleneck_move_out_of_way(unit_in_way, data)
|
||||
best_unit = unit_in_way
|
||||
data.BD_level_up_defender = nil
|
||||
data.BD_level_up_weapon = nil
|
||||
BD_level_up_defender = nil
|
||||
BD_level_up_weapon = nil
|
||||
end
|
||||
|
||||
data.BD_bottleneck_moves_done = false
|
||||
data.BD_unit, data.BD_hex = best_unit, best_hex
|
||||
BD_bottleneck_moves_done = false
|
||||
BD_unit, BD_hex = best_unit, best_hex
|
||||
end
|
||||
|
||||
return cfg.ca_score
|
||||
end
|
||||
|
||||
function ca_bottleneck_move:execution(cfg, data)
|
||||
if data.BD_bottleneck_moves_done then
|
||||
if BD_bottleneck_moves_done then
|
||||
local units = {}
|
||||
if MAISD.get_mai_self_data(data, cfg.ai_id, "side_leader_activated") then
|
||||
units = AH.get_units_with_moves { side = wesnoth.current.side }
|
||||
|
@ -466,16 +470,16 @@ function ca_bottleneck_move:execution(cfg, data)
|
|||
end
|
||||
else
|
||||
-- Don't want full move, as this might be stepping out of the way
|
||||
local cfg = { partial_move = true, weapon = data.BD_level_up_weapon }
|
||||
AH.robust_move_and_attack(ai, data.BD_unit, data.BD_hex, data.BD_level_up_defender, cfg)
|
||||
local cfg = { partial_move = true, weapon = BD_level_up_weapon }
|
||||
AH.robust_move_and_attack(ai, BD_unit, BD_hex, BD_level_up_defender, cfg)
|
||||
end
|
||||
|
||||
-- Now delete almost everything
|
||||
-- Keep only data.BD_is_my_territory because it is very expensive
|
||||
data.BD_unit, data.BD_hex = nil, nil
|
||||
data.BD_level_up_defender, data.BD_level_up_weapon = nil, nil
|
||||
data.BD_bottleneck_moves_done = nil
|
||||
data.BD_def_map, data.BD_healer_map, data.BD_leadership_map, data.BD_healing_map = nil, nil, nil, nil
|
||||
-- Keep only BD_is_my_territory because it is very expensive
|
||||
BD_unit, BD_hex = nil, nil
|
||||
BD_level_up_defender, BD_level_up_weapon = nil, nil
|
||||
BD_bottleneck_moves_done = nil
|
||||
BD_def_map, BD_healer_map, BD_leadership_map, BD_healing_map = nil, nil, nil, nil
|
||||
end
|
||||
|
||||
return ca_bottleneck_move
|
||||
|
|
|
@ -52,7 +52,7 @@ function ca_coward:execution(cfg)
|
|||
-- Store this weighting in the third field of each 'reach' element
|
||||
reach[i][3] = rating
|
||||
else
|
||||
reach[i][3] = -9e99
|
||||
reach[i][3] = - math.huge
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -76,7 +76,7 @@ function ca_coward:execution(cfg)
|
|||
|
||||
-- As final step, if there are more than one remaining locations,
|
||||
-- we take the one with the minimum score in the distance-from-enemy criterion
|
||||
local max_rating, best_hex = -9e99
|
||||
local max_rating, best_hex = - math.huge
|
||||
for _,pos in ipairs(best_overall) do
|
||||
if (pos[3] > max_rating) then
|
||||
max_rating, best_hex = pos[3], pos
|
||||
|
@ -88,7 +88,7 @@ function ca_coward:execution(cfg)
|
|||
|
||||
-- If 'attack_if_trapped' is set, the coward attacks the weakest unit it ends up next to
|
||||
if cfg.attack_if_trapped then
|
||||
local max_rating, best_target = -9e99
|
||||
local max_rating, best_target = - math.huge
|
||||
for xa,ya in H.adjacent_tiles(coward.x, coward.y) do
|
||||
local target = wesnoth.get_unit(xa, ya)
|
||||
if target and wesnoth.is_enemy(coward.side, target.side) then
|
||||
|
|
|
@ -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,
|
||||
|
@ -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')
|
||||
|
@ -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] = wesnoth.unit_resistance(unit_proxy, attack_type) / 100.
|
||||
end
|
||||
|
||||
return single_unit_info
|
||||
end
|
||||
|
||||
|
@ -187,7 +139,7 @@ function ca_fast_attack_utils.get_unit_copy(id, gamedata)
|
|||
|
||||
if (not gamedata.unit_copies[id]) then
|
||||
local unit_proxy = wesnoth.get_units { id = id }[1]
|
||||
gamedata.unit_copies[id] = wesnoth.copy_unit(unit_proxy)
|
||||
gamedata.unit_copies[id] = unit_proxy:clone()
|
||||
end
|
||||
|
||||
return gamedata.unit_copies[id]
|
||||
|
@ -209,34 +161,32 @@ function ca_fast_attack_utils.get_unit_defense(unit_copy, x, y, defense_maps)
|
|||
if (not defense_maps[unit_copy.id][x]) then defense_maps[unit_copy.id][x] = {} end
|
||||
|
||||
if (not defense_maps[unit_copy.id][x][y]) then
|
||||
local defense = (100. - wesnoth.unit_defense(unit_copy, wesnoth.get_terrain(x, y))) / 100.
|
||||
local defense = (100. - unit_copy:defense(wesnoth.get_terrain(x, y))) / 100.
|
||||
defense_maps[unit_copy.id][x][y] = { defense = defense }
|
||||
end
|
||||
|
||||
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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
|
|
@ -2,11 +2,14 @@ local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
|||
local FAU = wesnoth.require "ai/micro_ais/cas/ca_fast_attack_utils.lua"
|
||||
local LS = wesnoth.require "location_set"
|
||||
|
||||
local fast_combat_units, fast_combat_unit_i,fast_target, fast_dst
|
||||
local gamedata, move_cache
|
||||
|
||||
local ca_fast_combat = {}
|
||||
|
||||
function ca_fast_combat:evaluation(cfg, data)
|
||||
data.move_cache = { turn = wesnoth.current.turn }
|
||||
data.gamedata = FAU.gamedata_setup()
|
||||
move_cache = { turn = wesnoth.current.turn }
|
||||
gamedata = FAU.gamedata_setup()
|
||||
|
||||
local filter_own = wml.get_child(cfg, "filter")
|
||||
local filter_enemy = wml.get_child(cfg, "filter_second")
|
||||
|
@ -15,24 +18,24 @@ function ca_fast_combat:evaluation(cfg, data)
|
|||
local units_sorted = true
|
||||
if (not filter_own) and (not filter_enemy) then
|
||||
local attacks_aspect = ai.aspects.attacks
|
||||
if (not data.fast_combat_units) or (not data.fast_combat_units[1]) then
|
||||
if (not fast_combat_units) or (not fast_combat_units[1]) then
|
||||
-- Leader is dealt with in a separate CA
|
||||
data.fast_combat_units = {}
|
||||
fast_combat_units = {}
|
||||
for _,unit in ipairs(attacks_aspect.own) do
|
||||
if (not unit.canrecruit) then
|
||||
table.insert(data.fast_combat_units, unit)
|
||||
table.insert(fast_combat_units, unit)
|
||||
end
|
||||
end
|
||||
if (not data.fast_combat_units[1]) then return 0 end
|
||||
if (not fast_combat_units[1]) then return 0 end
|
||||
units_sorted = false
|
||||
end
|
||||
enemies = attacks_aspect.enemy
|
||||
else
|
||||
if (not data.fast_combat_units) or (not data.fast_combat_units[1]) then
|
||||
data.fast_combat_units = AH.get_live_units(
|
||||
if (not fast_combat_units) or (not fast_combat_units[1]) then
|
||||
fast_combat_units = AH.get_live_units(
|
||||
FAU.build_attack_filter("own", filter_own)
|
||||
)
|
||||
if (not data.fast_combat_units[1]) then return 0 end
|
||||
if (not fast_combat_units[1]) then return 0 end
|
||||
units_sorted = false
|
||||
end
|
||||
enemies = AH.get_live_units(
|
||||
|
@ -43,9 +46,9 @@ function ca_fast_combat:evaluation(cfg, data)
|
|||
if not units_sorted then
|
||||
-- For speed reasons, we'll go through the arrays from the end, so they are sorted backwards
|
||||
if cfg.weak_units_first then
|
||||
table.sort(data.fast_combat_units, function(a,b) return a.hitpoints > b.hitpoints end)
|
||||
table.sort(fast_combat_units, function(a,b) return a.hitpoints > b.hitpoints end)
|
||||
else
|
||||
table.sort(data.fast_combat_units, function(a,b) return a.hitpoints < b.hitpoints end)
|
||||
table.sort(fast_combat_units, function(a,b) return a.hitpoints < b.hitpoints end)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -71,43 +74,42 @@ 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)
|
||||
|
||||
for i = #data.fast_combat_units,1,-1 do
|
||||
local unit = data.fast_combat_units[i]
|
||||
for i = #fast_combat_units,1,-1 do
|
||||
local unit = fast_combat_units[i]
|
||||
|
||||
if unit and unit.valid and (unit.attacks_left > 0) and (#unit.attacks > 0) then
|
||||
local unit_info = FAU.get_unit_info(unit, data.gamedata)
|
||||
local unit_copy = FAU.get_unit_copy(unit.id, data.gamedata)
|
||||
local unit_info = FAU.get_unit_info(unit, gamedata)
|
||||
local unit_copy = FAU.get_unit_copy(unit.id, gamedata)
|
||||
local attacks = AH.get_attacks({ unit }, { include_occupied = cfg.include_occupied_attack_hexes, viewing_side = viewing_side })
|
||||
|
||||
if (#attacks > 0) then
|
||||
local max_rating, best_target, best_dst = -9e99
|
||||
local max_rating, best_target, best_dst = - math.huge
|
||||
for _,attack in ipairs(attacks) do
|
||||
if enemy_map:get(attack.target.x, attack.target.y)
|
||||
and (not avoid_map:get(attack.dst.x, attack.dst.y))
|
||||
then
|
||||
local target = wesnoth.get_unit(attack.target.x, attack.target.y)
|
||||
local target_info = FAU.get_unit_info(target, data.gamedata)
|
||||
local target_info = FAU.get_unit_info(target, gamedata)
|
||||
|
||||
local att_stat, def_stat = FAU.battle_outcome(
|
||||
unit_copy, target, { attack.dst.x, attack.dst.y },
|
||||
unit_info, target_info, data.gamedata, data.move_cache
|
||||
unit_info, target_info, gamedata, move_cache
|
||||
)
|
||||
|
||||
local rating, attacker_rating, defender_rating, extra_rating = FAU.attack_rating(
|
||||
{ unit_info }, target_info, { { attack.dst.x, attack.dst.y } },
|
||||
{ att_stat }, def_stat, data.gamedata,
|
||||
{ att_stat }, def_stat, 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
|
||||
|
@ -116,22 +118,23 @@ function ca_fast_combat:evaluation(cfg, data)
|
|||
end
|
||||
|
||||
if best_target then
|
||||
data.fast_combat_unit_i = i
|
||||
data.fast_target, data.fast_dst = best_target, best_dst
|
||||
fast_combat_unit_i = i
|
||||
fast_target, fast_dst = best_target, best_dst
|
||||
return cfg.ca_score
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
data.fast_combat_units[i] = nil
|
||||
fast_combat_units[i] = nil
|
||||
end
|
||||
|
||||
return 0
|
||||
end
|
||||
|
||||
function ca_fast_combat:execution(cfg, data)
|
||||
AH.robust_move_and_attack(ai, data.fast_combat_units[data.fast_combat_unit_i], data.fast_dst, data.fast_target)
|
||||
data.fast_combat_units[data.fast_combat_unit_i] = nil
|
||||
AH.robust_move_and_attack(ai, fast_combat_units[fast_combat_unit_i], fast_dst, fast_target)
|
||||
fast_combat_units[fast_combat_unit_i] = nil
|
||||
fast_combat_unit_i,fast_target, fast_dst = nil, nil, nil
|
||||
end
|
||||
|
||||
return ca_fast_combat
|
||||
|
|
|
@ -3,6 +3,9 @@ local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
|||
local FAU = wesnoth.require "ai/micro_ais/cas/ca_fast_attack_utils.lua"
|
||||
local LS = wesnoth.require "location_set"
|
||||
|
||||
local leader, fast_target, fast_dst
|
||||
local gamedata, move_cache
|
||||
|
||||
local ca_fast_combat_leader = {}
|
||||
|
||||
function ca_fast_combat_leader:evaluation(cfg, data)
|
||||
|
@ -15,8 +18,8 @@ function ca_fast_combat_leader:evaluation(cfg, data)
|
|||
leader_attack_max_units = (cfg and cfg.leader_attack_max_units) or 3
|
||||
leader_additional_threat = (cfg and cfg.leader_additional_threat) or 1
|
||||
|
||||
data.move_cache = { turn = wesnoth.current.turn }
|
||||
data.gamedata = FAU.gamedata_setup()
|
||||
move_cache = { turn = wesnoth.current.turn }
|
||||
gamedata = FAU.gamedata_setup()
|
||||
|
||||
local filter_own = wml.get_child(cfg, "filter")
|
||||
local filter_enemy = wml.get_child(cfg, "filter_second")
|
||||
|
@ -64,7 +67,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)
|
||||
|
@ -102,8 +104,8 @@ function ca_fast_combat_leader:evaluation(cfg, data)
|
|||
end
|
||||
end
|
||||
|
||||
local leader_info = FAU.get_unit_info(leader, data.gamedata)
|
||||
local leader_copy = FAU.get_unit_copy(leader.id, data.gamedata)
|
||||
local leader_info = FAU.get_unit_info(leader, gamedata)
|
||||
local leader_copy = FAU.get_unit_copy(leader.id, gamedata)
|
||||
|
||||
-- If threatened_leader_fights=yes, check out the current threat (power only,
|
||||
-- not units) on the AI leader
|
||||
|
@ -120,7 +122,7 @@ function ca_fast_combat_leader:evaluation(cfg, data)
|
|||
local attacks = AH.get_attacks({ leader }, { include_occupied = cfg.include_occupied_attack_hexes, viewing_side = viewing_side })
|
||||
|
||||
if (#attacks > 0) then
|
||||
local max_rating, best_target, best_dst = -9e99
|
||||
local max_rating, best_target, best_dst = - math.huge
|
||||
for _,attack in ipairs(attacks) do
|
||||
if enemy_map:get(attack.target.x, attack.target.y)
|
||||
and (not avoid_map:get(attack.dst.x, attack.dst.y))
|
||||
|
@ -150,23 +152,23 @@ function ca_fast_combat_leader:evaluation(cfg, data)
|
|||
|
||||
if acceptable_attack then
|
||||
local target = wesnoth.get_unit(attack.target.x, attack.target.y)
|
||||
local target_info = FAU.get_unit_info(target, data.gamedata)
|
||||
local target_info = FAU.get_unit_info(target, gamedata)
|
||||
|
||||
local att_stat, def_stat = FAU.battle_outcome(
|
||||
leader_copy, target, { attack.dst.x, attack.dst.y },
|
||||
leader_info, target_info, data.gamedata, data.move_cache
|
||||
leader_info, target_info, gamedata, move_cache
|
||||
)
|
||||
|
||||
local rating, attacker_rating, defender_rating, extra_rating = FAU.attack_rating(
|
||||
{ leader_info }, target_info, { { attack.dst.x, attack.dst.y } },
|
||||
{ att_stat }, def_stat, data.gamedata,
|
||||
{ att_stat }, def_stat, 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
|
||||
|
@ -176,8 +178,8 @@ function ca_fast_combat_leader:evaluation(cfg, data)
|
|||
end
|
||||
|
||||
if best_target then
|
||||
data.leader = leader
|
||||
data.fast_target, data.fast_dst = best_target, best_dst
|
||||
leader = leader
|
||||
fast_target, fast_dst = best_target, best_dst
|
||||
return cfg.ca_score
|
||||
end
|
||||
end
|
||||
|
@ -186,8 +188,8 @@ function ca_fast_combat_leader:evaluation(cfg, data)
|
|||
end
|
||||
|
||||
function ca_fast_combat_leader:execution(cfg, data)
|
||||
AH.robust_move_and_attack(ai, data.leader, data.fast_dst, data.fast_target)
|
||||
data.leader, data.fast_target, data.fast_dst = nil, nil, nil
|
||||
AH.robust_move_and_attack(ai, leader, fast_dst, fast_target)
|
||||
leader, fast_target, fast_dst = nil, nil, nil
|
||||
end
|
||||
|
||||
return ca_fast_combat_leader
|
||||
|
|
|
@ -143,7 +143,7 @@ function ca_fast_move:execution(cfg)
|
|||
if (next_goal > #goals) then next_goal = 1 end
|
||||
local goal = goals[next_goal]
|
||||
|
||||
local max_rating, best_unit_info = -9e99
|
||||
local max_rating, best_unit_info = - math.huge
|
||||
for _,unit_info in ipairs(goal) do
|
||||
if (not unit_info.cost) then
|
||||
local _,cost =
|
||||
|
@ -198,7 +198,7 @@ function ca_fast_move:execution(cfg)
|
|||
local reach = wesnoth.find_reach(unit)
|
||||
|
||||
local pre_ratings = {}
|
||||
local max_rating, best_hex = -9e99
|
||||
local max_rating, best_hex = - math.huge
|
||||
for _,loc in ipairs(reach) do
|
||||
if (not avoid_map:get(loc[1], loc[2])) then
|
||||
local rating = -M.distance_between(loc[1], loc[2], short_goal[1], short_goal[2])
|
||||
|
@ -243,10 +243,10 @@ function ca_fast_move:execution(cfg)
|
|||
if cfg.dungeon_mode then
|
||||
table.sort(pre_ratings, function(a,b) return (a.rating > b.rating) end)
|
||||
|
||||
wesnoth.extract_unit(unit)
|
||||
unit:extract()
|
||||
local old_x, old_y = unit.x, unit.y
|
||||
|
||||
local max_rating = -9e99
|
||||
local max_rating = - math.huge
|
||||
for _,pre_rating in ipairs(pre_ratings) do
|
||||
-- If pre_rating is worse than the full rating, we are done because the
|
||||
-- move cost can never be less than the distance, so we cannot possibly do
|
||||
|
@ -263,8 +263,7 @@ function ca_fast_move:execution(cfg)
|
|||
end
|
||||
end
|
||||
|
||||
unit.x, unit.y = old_x, old_y
|
||||
wesnoth.put_unit(unit)
|
||||
unit:to_map(old_x, old_y)
|
||||
end
|
||||
|
||||
if best_hex then
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local LS = wesnoth.require "location_set"
|
||||
local M = wesnoth.map
|
||||
local T = wml.tag
|
||||
|
||||
local function get_forest_animals(cfg)
|
||||
-- We want the deer/rabbits to move first, tuskers afterward
|
||||
|
@ -74,12 +73,7 @@ function ca_forest_animals_move:execution(cfg)
|
|||
local wander_terrain = wml.get_child(cfg, "filter_location") or {}
|
||||
if (not close_enemies[1]) then
|
||||
local reach = AH.get_reachable_unocc(unit)
|
||||
local width, height = wesnoth.get_map_size()
|
||||
local wander_locs = wesnoth.get_locations {
|
||||
x = '1-' .. width,
|
||||
y = '1-' .. height,
|
||||
{ "and", wander_terrain }
|
||||
}
|
||||
local wander_locs = AH.get_locations_no_borders(wander_terrain)
|
||||
local locs_map = LS.of_pairs(wander_locs)
|
||||
|
||||
local reachable_wander_terrain = {}
|
||||
|
@ -97,7 +91,7 @@ function ca_forest_animals_move:execution(cfg)
|
|||
AH.checked_move(ai, unit, reachable_wander_terrain[rand][1], reachable_wander_terrain[rand][2])
|
||||
end
|
||||
else -- Or if no close reachable terrain was found, move toward the closest
|
||||
local min_dist, best_hex = 9e99
|
||||
local min_dist, best_hex = math.huge
|
||||
for _,loc in ipairs(wander_locs) do
|
||||
local dist = M.distance_between(loc[1], loc[2], unit.x, unit.y)
|
||||
if dist < min_dist then
|
||||
|
@ -156,7 +150,7 @@ function ca_forest_animals_move:execution(cfg)
|
|||
|
||||
-- If this is a rabbit ending on a hole -> disappears
|
||||
if (unit.type == rabbit_type) and hole_map:get(farthest_hex[1], farthest_hex[2]) then
|
||||
wesnoth.invoke_synced_command("rabbit_despawn", { x = farthest_hex[1], y = farthest_hex[2]})
|
||||
wesnoth.invoke_synced_command("rabbit_despawn", { x = farthest_hex[1], y = farthest_hex[2]})
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local T = wml.tag
|
||||
|
||||
local ca_forest_animals_new_rabbit = {}
|
||||
|
||||
|
@ -57,7 +56,7 @@ function ca_forest_animals_new_rabbit:execution(cfg)
|
|||
x, y = wesnoth.find_vacant_tile(holes[i].x, holes[i].y)
|
||||
end
|
||||
|
||||
wesnoth.invoke_synced_command("rabbit_spawn", { rabbit_type = cfg.rabbit_type, x = x, y = y})
|
||||
wesnoth.invoke_synced_command("rabbit_spawn", { rabbit_type = cfg.rabbit_type, x = x, y = y})
|
||||
end
|
||||
|
||||
if wesnoth.sides[wesnoth.current.side].shroud then
|
||||
|
|
|
@ -32,7 +32,7 @@ function ca_forest_animals_tusker_attack:execution(cfg)
|
|||
local adjacent_enemies = get_adjacent_enemies(cfg)
|
||||
|
||||
-- Find the closest enemy to any tusker
|
||||
local min_dist, attacker, target = 9e99
|
||||
local min_dist, attacker, target = math.huge
|
||||
for _,tusker in ipairs(tuskers) do
|
||||
for _,enemy in ipairs(adjacent_enemies) do
|
||||
local dist = M.distance_between(tusker.x, tusker.y, enemy.x, enemy.y)
|
||||
|
|
|
@ -33,7 +33,7 @@ function ca_forest_animals_tusklet_move:execution(cfg)
|
|||
local tusklet = get_tusklets(cfg)[1]
|
||||
local tuskers = get_tuskers(cfg)
|
||||
|
||||
local goto_tusker, min_dist = {}, 9e99
|
||||
local min_dist, goto_tusker = math.huge
|
||||
for _,tusker in ipairs(tuskers) do
|
||||
local dist = M.distance_between(tusker.x, tusker.y, tusklet.x, tusklet.y)
|
||||
if (dist < min_dist) then
|
||||
|
|
|
@ -8,7 +8,7 @@ local M = wesnoth.map
|
|||
|
||||
local function custom_cost(x, y, unit, enemy_map, enemy_attack_map, multiplier)
|
||||
local terrain = wesnoth.get_terrain(x, y)
|
||||
local move_cost = wesnoth.unit_movement_cost(unit, terrain)
|
||||
local move_cost = unit:movement(terrain)
|
||||
|
||||
move_cost = move_cost + (enemy_map:get(x,y) or 0)
|
||||
move_cost = move_cost + (enemy_attack_map.units:get(x,y) or 0) * multiplier
|
||||
|
@ -45,12 +45,7 @@ function ca_goto:evaluation(cfg, data)
|
|||
|
||||
-- For convenience, we check for locations here, and just pass that to the exec function
|
||||
-- This is mostly to make the unique_goals option easier
|
||||
local width, height = wesnoth.get_map_size()
|
||||
local all_locs = wesnoth.get_locations {
|
||||
x = '1-' .. width,
|
||||
y = '1-' .. height,
|
||||
{ "and", wml.get_child(cfg, "filter_location") }
|
||||
}
|
||||
local all_locs = AH.get_locations_no_borders(wml.get_child(cfg, "filter_location"))
|
||||
if (#all_locs == 0) then return 0 end
|
||||
|
||||
-- If 'unique_goals' is set, check whether there are locations left to go to.
|
||||
|
@ -118,7 +113,7 @@ function ca_goto:execution(cfg, data)
|
|||
enemy_attack_map = BC.get_attack_map(live_enemies)
|
||||
end
|
||||
|
||||
local max_rating, closest_hex, best_path, best_unit = -9e99
|
||||
local max_rating, closest_hex, best_path, best_unit = - math.huge
|
||||
for _,unit in ipairs(units) do
|
||||
for _,loc in ipairs(locs) do
|
||||
-- If cfg.use_straight_line is set, we simply find the closest
|
||||
|
@ -149,14 +144,14 @@ function ca_goto:execution(cfg, data)
|
|||
if cfg.ignore_enemy_at_goal then
|
||||
enemy_at_goal = wesnoth.get_unit(loc[1], loc[2])
|
||||
if enemy_at_goal and wesnoth.is_enemy(wesnoth.current.side, enemy_at_goal.side) then
|
||||
wesnoth.extract_unit(enemy_at_goal)
|
||||
enemy_at_goal:extract()
|
||||
else
|
||||
enemy_at_goal = nil
|
||||
end
|
||||
end
|
||||
path, cost = AH.find_path_with_shroud(unit, loc[1], loc[2], { ignore_units = cfg.ignore_units })
|
||||
if enemy_at_goal then
|
||||
wesnoth.put_unit(enemy_at_goal)
|
||||
enemy_at_goal:to_map()
|
||||
--- Give massive penalty for this goal hex
|
||||
cost = cost + 100
|
||||
end
|
||||
|
|
|
@ -49,12 +49,7 @@ function ca_hang_out:execution(cfg)
|
|||
{ "filter", { side = wesnoth.current.side, canrecruit = "yes" } }
|
||||
}
|
||||
|
||||
local width, height = wesnoth.get_map_size()
|
||||
local locs = wesnoth.get_locations {
|
||||
x = '1-' .. width,
|
||||
y = '1-' .. height,
|
||||
{ "and", filter_location }
|
||||
}
|
||||
local locs = AH.get_locations_no_borders(filter_location)
|
||||
|
||||
-- Get map of locations to be avoided.
|
||||
-- Use [micro_ai][avoid] tag with priority over [ai][avoid].
|
||||
|
@ -79,15 +74,15 @@ function ca_hang_out:execution(cfg)
|
|||
end
|
||||
end
|
||||
end
|
||||
if avoid_map == nil then
|
||||
if not avoid_map then
|
||||
avoid_map = LS.of_pairs(wesnoth.get_locations { terrain = 'C*,C*^*,*^C*' })
|
||||
end
|
||||
|
||||
local max_rating, best_hex, best_unit = -9e99
|
||||
local max_rating, best_hex, best_unit = - math.huge
|
||||
for _,unit in ipairs(units) do
|
||||
-- Only consider units that have not been marked yet
|
||||
if (not MAIUV.get_mai_unit_variables(unit, cfg.ai_id, "moved")) then
|
||||
local max_rating_unit, best_hex_unit = -9e99
|
||||
local max_rating_unit, best_hex_unit = - math.huge
|
||||
|
||||
-- Check out all unoccupied hexes the unit can reach
|
||||
local reach_map = AH.get_reachable_unocc(unit)
|
||||
|
|
|
@ -38,7 +38,7 @@ function ca_healer_move:evaluation(cfg, data)
|
|||
-- Potential healees are units without MP that don't already have a healer (also without MP) next to them
|
||||
-- Also, they cannot be on a village or regenerate
|
||||
if (healee.moves == 0) then
|
||||
if (not wesnoth.match_unit(healee, { ability = "regenerates" })) then
|
||||
if (not healee:matches { ability = "regenerates" }) then
|
||||
local is_village = wesnoth.get_terrain_info(wesnoth.get_terrain(healee.x, healee.y)).village
|
||||
if (not is_village) then
|
||||
local is_healee = true
|
||||
|
@ -57,13 +57,13 @@ function ca_healer_move:evaluation(cfg, data)
|
|||
end
|
||||
|
||||
local enemies = AH.get_attackable_enemies()
|
||||
for _,healee in ipairs(healees_MP) do wesnoth.extract_unit(healee) end
|
||||
for _,healee in ipairs(healees_MP) do healee:extract() end
|
||||
local enemy_attack_map = BC.get_attack_map(enemies)
|
||||
for _,healee in ipairs(healees_MP) do wesnoth.put_unit(healee) end
|
||||
for _,healee in ipairs(healees_MP) do healee:to_map() end
|
||||
|
||||
local avoid_map = LS.of_pairs(ai.aspects.avoid)
|
||||
|
||||
local max_rating = -9e99
|
||||
local max_rating = - math.huge
|
||||
for _,healer in ipairs(healers) do
|
||||
local reach = wesnoth.find_reach(healer)
|
||||
|
||||
|
@ -103,10 +103,11 @@ function ca_healer_move:evaluation(cfg, data)
|
|||
rating = rating - enemies_in_reach * 1000
|
||||
|
||||
-- All else being more or less equal, prefer villages and strong terrain
|
||||
local is_village = wesnoth.get_terrain_info(wesnoth.get_terrain(loc[1], loc[2])).village
|
||||
local terrain = wesnoth.get_terrain(loc[1], loc[2])
|
||||
local is_village = wesnoth.get_terrain_info(terrain).village
|
||||
if is_village then rating = rating + 2 end
|
||||
|
||||
local defense = 100 - wesnoth.unit_defense(healer, wesnoth.get_terrain(loc[1], loc[2]))
|
||||
local defense = 100 - healer:defense(terrain)
|
||||
rating = rating + defense / 10.
|
||||
end
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ function ca_herding_attack_close_enemy:execution(cfg)
|
|||
local radius = cfg.attack_distance or 4
|
||||
local enemies = get_enemies(cfg, radius)
|
||||
|
||||
max_rating, best_dog, best_enemy, best_hex = -9e99
|
||||
local max_rating, best_dog, best_enemy, best_hex = - math.huge
|
||||
for _,enemy in ipairs(enemies) do
|
||||
for _,dog in ipairs(dogs) do
|
||||
local reach_map = AH.get_reachable_unocc(dog)
|
||||
|
@ -90,7 +90,7 @@ function ca_herding_attack_close_enemy:execution(cfg)
|
|||
if (not dogs[1]) then return end
|
||||
|
||||
-- Find closest sheep/enemy pair first
|
||||
local min_dist, closest_sheep, closest_enemy = 9e99
|
||||
local min_dist, closest_sheep, closest_enemy = math.huge
|
||||
for _,enemy in ipairs(enemies) do
|
||||
for _,single_sheep in ipairs(sheep) do
|
||||
local dist = M.distance_between(enemy.x, enemy.y, single_sheep.x, single_sheep.y)
|
||||
|
@ -102,7 +102,7 @@ function ca_herding_attack_close_enemy:execution(cfg)
|
|||
end
|
||||
|
||||
-- Move dogs in between enemies and sheep
|
||||
max_rating, best_dog, best_hex = -9e99
|
||||
local max_rating, best_dog, best_hex = - math.huge
|
||||
for _,dog in ipairs(dogs) do
|
||||
local reach_map = AH.get_reachable_unocc(dog)
|
||||
reach_map:iter( function(x, y, v)
|
||||
|
|
|
@ -42,7 +42,7 @@ function ca_herding_herd_sheep:execution(cfg)
|
|||
local dogs = get_dogs(cfg)
|
||||
local sheep_to_herd = get_sheep_to_herd(cfg)
|
||||
|
||||
local max_rating, best_dog, best_hex = -9e99
|
||||
local max_rating, best_dog, best_hex = - math.huge
|
||||
local c_x, c_y = cfg.herd_x, cfg.herd_y
|
||||
for _,single_sheep in ipairs(sheep_to_herd) do
|
||||
-- Farthest sheep goes first
|
||||
|
@ -73,11 +73,7 @@ function ca_herding_herd_sheep:execution(cfg)
|
|||
end
|
||||
end
|
||||
|
||||
if (best_hex[1] == best_dog.x) and (best_hex[2] == best_dog.y) then
|
||||
AH.checked_stopunit_moves(ai, best_dog)
|
||||
else
|
||||
AH.checked_move(ai, best_dog, best_hex[1], best_hex[2]) -- partial move only!
|
||||
end
|
||||
AH.robust_move_and_attack(ai, best_dog, best_hex, nil, { partial_move = true })
|
||||
end
|
||||
|
||||
return ca_herding_herd_sheep
|
||||
|
|
|
@ -29,7 +29,7 @@ function ca_herding_sheep_move:execution(cfg)
|
|||
reach_map:iter( function(x, y, v)
|
||||
for xa, ya in H.adjacent_tiles(x, y) do
|
||||
local dog = wesnoth.get_unit(xa, ya)
|
||||
if dog and (wesnoth.match_unit(dog, dogs_filter)) then
|
||||
if dog and dog:matches(dogs_filter) then
|
||||
reach_map:remove(x, y)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -12,7 +12,7 @@ local function hunter_attack_weakest_adj_enemy(ai, hunter)
|
|||
|
||||
if (hunter.attacks_left == 0) then return 'no_attack' end
|
||||
|
||||
local min_hp, target = 9e99
|
||||
local min_hp, target = math.huge
|
||||
for xa,ya in H.adjacent_tiles(hunter.x, hunter.y) do
|
||||
local enemy = wesnoth.get_unit(xa, ya)
|
||||
if AH.is_attackable_enemy(enemy) then
|
||||
|
@ -74,7 +74,7 @@ function ca_hunter:execution(cfg)
|
|||
local reach_map = AH.get_reachable_unocc(hunter)
|
||||
|
||||
-- Now find the one of these hexes that is closest to the goal
|
||||
local max_rating, best_hex = -9e99
|
||||
local max_rating, best_hex = - math.huge
|
||||
reach_map:iter( function(x, y, v)
|
||||
-- Distance from goal is first rating
|
||||
local rating = -M.distance_between(x, y, hunter_vars.goal_x, hunter_vars.goal_y)
|
||||
|
|
|
@ -55,7 +55,7 @@ local function messenger_find_clearing_attack(messenger, goal_x, goal_y, cfg)
|
|||
|
||||
local attacks = AH.get_attacks(units, { simulate_combat = true })
|
||||
|
||||
local max_rating = -9e99
|
||||
local max_rating = - math.huge
|
||||
for _,attack in ipairs(attacks) do
|
||||
if (attack.target.x == enemy_in_way.x) and (attack.target.y == enemy_in_way.y) then
|
||||
|
||||
|
|
|
@ -35,7 +35,7 @@ function ca_messenger_escort_move:execution(cfg)
|
|||
local enemies = AH.get_attackable_enemies()
|
||||
|
||||
local base_rating_map = LS.create()
|
||||
local max_rating, best_unit, best_hex = -9e99
|
||||
local max_rating, best_unit, best_hex = - math.huge
|
||||
for _,unit in ipairs(escorts) do
|
||||
-- Only considering hexes unoccupied by other units is good enough for this
|
||||
local reach_map = AH.get_reachable_unocc(unit)
|
||||
|
@ -51,7 +51,7 @@ function ca_messenger_escort_move:execution(cfg)
|
|||
|
||||
-- Distance from messenger is most important; only closest messenger counts for this
|
||||
-- Give somewhat of a bonus for the messenger that has moved the farthest through the waypoints
|
||||
local max_messenger_rating = -9e99
|
||||
local max_messenger_rating = - math.huge
|
||||
for _,m in ipairs(messengers) do
|
||||
local messenger_rating = 1. / (M.distance_between(x, y, m.x, m.y) + 2.)
|
||||
local wp_rating = MAIUV.get_mai_unit_variables(m, cfg.ai_id, "wp_rating")
|
||||
|
|
|
@ -22,7 +22,7 @@ return function(cfg)
|
|||
|
||||
-- Set the next waypoint for all messengers
|
||||
-- Also find those with MP left and return the one to next move, together with the WP to move toward
|
||||
local max_rating, best_messenger, x, y = -9e99
|
||||
local max_rating, best_messenger, x, y = - math.huge
|
||||
for _,messenger in ipairs(messengers) do
|
||||
-- To avoid code duplication and ensure consistency, we store some pieces of
|
||||
-- information in the messenger units, even though it could be calculated each time it is needed
|
||||
|
|
|
@ -57,21 +57,21 @@ function ca_messenger_move:execution(cfg)
|
|||
|
||||
local unit_in_way = wesnoth.get_unit(next_hop[1], next_hop[2])
|
||||
if (unit_in_way == messenger) then unit_in_way = nil end
|
||||
if unit_in_way then wesnoth.extract_unit(unit_in_way) end
|
||||
if unit_in_way then unit_in_way:extract() end
|
||||
|
||||
wesnoth.put_unit(messenger, next_hop[1], next_hop[2])
|
||||
messenger.loc = { next_hop[1], next_hop[2] }
|
||||
local _, cost1 = AH.find_path_with_shroud(messenger, x, y, { ignore_units = 'yes' })
|
||||
|
||||
local unit_in_way2 = wesnoth.get_unit(optimum_hop[1], optimum_hop[2])
|
||||
if (unit_in_way2 == messenger) then unit_in_way2 = nil end
|
||||
if unit_in_way2 then wesnoth.extract_unit(unit_in_way2) end
|
||||
if unit_in_way2 then unit_in_way2:extract() end
|
||||
|
||||
wesnoth.put_unit(messenger, optimum_hop[1], optimum_hop[2])
|
||||
messenger.loc = { optimum_hop[1], optimum_hop[2] }
|
||||
local _, cost2 = AH.find_path_with_shroud(messenger, x, y, { ignore_units = 'yes' })
|
||||
|
||||
wesnoth.put_unit(messenger, x_current, y_current)
|
||||
if unit_in_way then wesnoth.put_unit(unit_in_way) end
|
||||
if unit_in_way2 then wesnoth.put_unit(unit_in_way2) end
|
||||
messenger.loc = { x_current, y_current }
|
||||
if unit_in_way then unit_in_way:to_map() end
|
||||
if unit_in_way2 then unit_in_way2:to_map() end
|
||||
|
||||
-- If cost2 is significantly less, that means that the optimum path might
|
||||
-- overall be faster even though it is currently blocked
|
||||
|
@ -90,12 +90,12 @@ function ca_messenger_move:execution(cfg)
|
|||
|
||||
local targets = AH.get_attackable_enemies { { "filter_adjacent", { id = messenger.id } } }
|
||||
|
||||
local max_rating, best_target, best_weapon = -9e99
|
||||
local max_rating, best_target, best_weapon = - math.huge
|
||||
for _,target in ipairs(targets) do
|
||||
for n_weapon,weapon in ipairs(messenger.attacks) do
|
||||
local att_stats, def_stats = wesnoth.simulate_combat(messenger, n_weapon, target)
|
||||
|
||||
local rating = -9e99
|
||||
local rating = - math.huge
|
||||
-- This is an acceptable attack if:
|
||||
-- 1. There is no counter attack
|
||||
-- 2. Probability of death is >=67% for enemy, 0% for attacker (default values)
|
||||
|
|
|
@ -29,7 +29,7 @@ function ca_protect_unit_attack:evaluation(cfg)
|
|||
-- Set up a counter attack damage table, as many pairs of attacks will be the same
|
||||
local counter_damage_table = {}
|
||||
|
||||
local max_rating = -9e99
|
||||
local max_rating = - math.huge
|
||||
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)
|
||||
|
@ -70,7 +70,7 @@ function ca_protect_unit_attack:evaluation(cfg)
|
|||
end
|
||||
|
||||
-- Add this to damage possible on this attack
|
||||
local min_hp = 1000
|
||||
local min_hp = math.huge
|
||||
for hp,chance in pairs(attack.att_stats.hp_chance) do
|
||||
if (chance > 0) and (hp < min_hp) then
|
||||
min_hp = hp
|
||||
|
|
|
@ -23,7 +23,7 @@ function ca_protect_unit_move:execution(cfg, data)
|
|||
|
||||
-- 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
|
||||
for _,unit in ipairs(protected_units) do unit:extract() end
|
||||
|
||||
local units = wesnoth.get_units { side = wesnoth.current.side }
|
||||
local enemy_units = AH.get_attackable_enemies()
|
||||
|
@ -32,7 +32,7 @@ function ca_protect_unit_move:execution(cfg, data)
|
|||
local enemy_attack_map = BC.get_attack_map(enemy_units).units -- enemy attack map
|
||||
|
||||
-- Now put the protected units back out there
|
||||
for _,unit in ipairs(protected_units) do wesnoth.put_unit(unit) end
|
||||
for _,unit in ipairs(protected_units) do unit:to_map() end
|
||||
|
||||
-- We move the weakest (fewest HP unit) first
|
||||
local unit = AH.choose(protected_units, function(u) return - u.hitpoints end)
|
||||
|
@ -46,7 +46,7 @@ function ca_protect_unit_move:execution(cfg, data)
|
|||
|
||||
local terrain_defense_map = LS.create()
|
||||
reach_map:iter(function(x, y, data)
|
||||
terrain_defense_map:insert(x, y, 100 - wesnoth.unit_defense(unit, wesnoth.get_terrain(x, y)))
|
||||
terrain_defense_map:insert(x, y, 100 - unit:defense(wesnoth.get_terrain(x, y)))
|
||||
end)
|
||||
|
||||
local goal_distance_map = LS.create()
|
||||
|
@ -55,10 +55,10 @@ function ca_protect_unit_move:execution(cfg, data)
|
|||
end)
|
||||
|
||||
-- Configuration parameters (no option to change these enabled at the moment)
|
||||
local enemy_weight = data.PU_enemy_weight or 100.
|
||||
local my_unit_weight = data.PU_my_unit_weight or 1.
|
||||
local distance_weight = data.PU_distance_weight or 3.
|
||||
local terrain_weight = data.PU_terrain_weight or 0.1
|
||||
local enemy_weight = 100.
|
||||
local my_unit_weight = 1.
|
||||
local distance_weight = 3.
|
||||
local terrain_weight = 0.1
|
||||
|
||||
-- If there are no enemies left, only distance to goal matters
|
||||
-- This is to avoid rare situations where moving toward goal rating is canceled by rating for moving away from own units
|
||||
|
@ -69,7 +69,7 @@ function ca_protect_unit_move:execution(cfg, data)
|
|||
terrain_weight = 0
|
||||
end
|
||||
|
||||
local max_rating, best_hex = -9e99
|
||||
local max_rating, best_hex = - math.huge
|
||||
for ind,_ in pairs(reach_map.values) do
|
||||
local rating =
|
||||
(attack_map.values[ind] or 0) * my_unit_weight
|
||||
|
|
|
@ -27,7 +27,7 @@ function ca_simple_attack:evaluation(cfg)
|
|||
local attacks = AH.get_attacks(units, { include_occupied = true })
|
||||
if (not attacks[1]) then return 0 end
|
||||
|
||||
local max_rating = -9e99
|
||||
local max_rating = - math.huge
|
||||
for _, att in ipairs(attacks) do
|
||||
local valid_target = true
|
||||
if enemy_filter and (not enemy_map:get(att.target.x, att.target.y)) then
|
||||
|
|
|
@ -37,7 +37,7 @@ function ca_stationed_guardian:execution(cfg)
|
|||
-- Otherwise, guardian will either attack or move toward station
|
||||
-- Enemies must be within cfg.distance of guardian, (s_x, s_y) *and* (g_x, g_y)
|
||||
-- simultaneously for guardian to attack
|
||||
local min_dist, target = 9e99
|
||||
local min_dist, target = math.huge
|
||||
for _,enemy in ipairs(enemies) do
|
||||
local dist_s = M.distance_between(cfg.station_x, cfg.station_y, enemy.x, enemy.y)
|
||||
local dist_g = M.distance_between(cfg.guard_x or cfg.station_x, cfg.guard_y or cfg.station_y, enemy.x, enemy.y)
|
||||
|
@ -52,14 +52,14 @@ function ca_stationed_guardian:execution(cfg)
|
|||
if target then
|
||||
-- Find tiles adjacent to the target
|
||||
-- Save the one with the highest defense rating that guardian can reach
|
||||
local best_defense, attack_loc = -9e99
|
||||
local best_defense, attack_loc = - math.huge
|
||||
for xa,ya in H.adjacent_tiles(target.x, target.y) do
|
||||
-- Only consider unoccupied hexes
|
||||
local unit_in_way = wesnoth.get_unit(xa, ya)
|
||||
if (not AH.is_visible_unit(wesnoth.current.side, unit_in_way))
|
||||
or (unit_in_way == guardian)
|
||||
then
|
||||
local defense = 100 - wesnoth.unit_defense(guardian, wesnoth.get_terrain(xa, ya))
|
||||
local defense = 100 - guardian:defense(wesnoth.get_terrain(xa, ya))
|
||||
local nh = AH.next_hop(guardian, xa, ya)
|
||||
if nh then
|
||||
if (nh[1] == xa) and (nh[2] == ya) and (defense > best_defense) then
|
||||
|
@ -77,7 +77,7 @@ function ca_stationed_guardian:execution(cfg)
|
|||
|
||||
-- Go through all hexes the guardian can reach, find closest to target
|
||||
-- Cannot use next_hop here since target hex is occupied by enemy
|
||||
local min_dist, nh = 9e99
|
||||
local min_dist, nh = math.huge
|
||||
for _,hex in ipairs(reach) do
|
||||
-- Only consider unoccupied hexes
|
||||
local unit_in_way = wesnoth.get_unit(hex[1], hex[2])
|
||||
|
|
|
@ -34,7 +34,7 @@ function ca_wolves_move:execution(cfg)
|
|||
local avoid_map = BC.get_attack_map(avoid_units).units
|
||||
|
||||
-- Find prey that is closest to the wolves
|
||||
local min_dist, target = 9e99
|
||||
local min_dist, target = math.huge
|
||||
for _,prey_unit in ipairs(prey) do
|
||||
local dist = 0
|
||||
for _,wolf in ipairs(wolves) do
|
||||
|
|
|
@ -76,7 +76,7 @@ function ca_wolves_multipacks_attack:execution(cfg)
|
|||
end
|
||||
|
||||
-- Find which target can be attacked by the most units, from the most hexes; and rate by fewest HP if equal
|
||||
local max_rating, best_target = -9e99
|
||||
local max_rating, best_target = - math.huge
|
||||
for attack_ind,attack in pairs(attack_map_wolves) do
|
||||
local number_wolves, number_hexes = 0, 0
|
||||
for _,w in pairs(attack) do number_wolves = number_wolves + 1 end
|
||||
|
@ -110,7 +110,7 @@ function ca_wolves_multipacks_attack:execution(cfg)
|
|||
-- Now we know the best target and need to attack
|
||||
-- This is done on a wolf-by-wolf basis, the outside while loop taking care of
|
||||
-- the next wolf in the pack on subsequent iterations
|
||||
local max_rating, best_attack = -9e99
|
||||
local max_rating, best_attack = - math.huge
|
||||
for _,attack in ipairs(attacks) do
|
||||
if (attack.target.x == best_target.x) and (attack.target.y == best_target.y) then
|
||||
local rating = attack.att_stats.average_hp / 2. - attack.def_stats.average_hp
|
||||
|
|
|
@ -58,7 +58,7 @@ function wolves_multipacks_functions.assign_packs(cfg)
|
|||
-- First, go through packs that have less than pack_size members
|
||||
for pack_number,pack in pairs(packs) do
|
||||
if (#pack < pack_size) then
|
||||
local min_dist, best_wolf, best_ind = 9e99
|
||||
local min_dist, best_wolf, best_ind = math.huge
|
||||
for ind,wolf in ipairs(nopack_wolves) do
|
||||
-- Criterion is distance from the first two wolves of the pack
|
||||
local dist1 = M.distance_between(wolf.x, wolf.y, pack[1].x, pack[1].y)
|
||||
|
@ -97,7 +97,7 @@ function wolves_multipacks_functions.assign_packs(cfg)
|
|||
-- They form the next pack
|
||||
local new_pack_wolves = {}
|
||||
while (#new_pack_wolves < pack_size) do
|
||||
local min_dist, best_wolf, best_wolf_ind = 9e99
|
||||
local min_dist, best_wolf, best_wolf_ind = math.huge
|
||||
for ind,nopack_wolf in ipairs(nopack_wolves) do
|
||||
local dist = 0
|
||||
for _,pack_wolf in ipairs(new_pack_wolves) do
|
||||
|
|
|
@ -43,8 +43,7 @@ function ca_wolves_multipacks_wander:execution(cfg)
|
|||
-- Pack gets a new goal if none exist or on any move with 10% random chance
|
||||
local rand = math.random(10)
|
||||
if (not goal[1]) or (rand == 1) then
|
||||
local width, height = wesnoth.get_map_size()
|
||||
local locs = wesnoth.get_locations { x = '1-'..width, y = '1-'..height }
|
||||
local locs = AH.get_locations_no_borders {}
|
||||
|
||||
-- Need to find reachable terrain for this to be a viable goal
|
||||
-- We only check whether the first wolf can get there
|
||||
|
@ -81,7 +80,7 @@ function ca_wolves_multipacks_wander:execution(cfg)
|
|||
|
||||
-- Keep only those hexes that can be reached by all wolves in the pack
|
||||
-- and add distance from goal for those
|
||||
local max_rating, goto_hex = -9e99
|
||||
local max_rating, goto_hex = - math.huge
|
||||
reach_map:iter( function(x, y, v)
|
||||
local rating = reach_map:get(x, y)
|
||||
if (rating == #pack * 100) then
|
||||
|
|
|
@ -31,7 +31,7 @@ function ca_wolves_wander:execution(cfg)
|
|||
local avoid_units = AH.get_attackable_enemies({ type = cfg.avoid_type })
|
||||
local avoid_map = BC.get_attack_map(avoid_units).units
|
||||
|
||||
local max_rating, goal_hex = -9e99
|
||||
local max_rating, goal_hex = - math.huge
|
||||
reach_map:iter( function (x, y, v)
|
||||
local rating = v + math.random(99)/100.
|
||||
if avoid_map:get(x, y) then rating = rating - 1000 end
|
||||
|
|
|
@ -27,7 +27,7 @@ function ca_zone_guardian:execution(cfg)
|
|||
local zone_enemy = wml.get_child(cfg, "filter_location_enemy") or zone
|
||||
local enemies = AH.get_attackable_enemies { { "filter_location", zone_enemy } }
|
||||
if enemies[1] then
|
||||
local min_dist, target = 9e99
|
||||
local min_dist, target = math.huge
|
||||
for _,enemy in ipairs(enemies) do
|
||||
local dist = M.distance_between(guardian.x, guardian.y, enemy.x, enemy.y)
|
||||
if (dist < min_dist) then
|
||||
|
@ -39,14 +39,14 @@ function ca_zone_guardian:execution(cfg)
|
|||
if target then
|
||||
-- Find tiles adjacent to the target
|
||||
-- Save the one with the highest defense rating that guardian can reach
|
||||
local best_defense, attack_loc = -9e99
|
||||
local best_defense, attack_loc = - math.huge
|
||||
for xa,ya in H.adjacent_tiles(target.x, target.y) do
|
||||
-- Only consider unoccupied hexes
|
||||
local unit_in_way = wesnoth.get_unit(xa, ya)
|
||||
if (not AH.is_visible_unit(wesnoth.current.side, unit_in_way))
|
||||
or (unit_in_way == guardian)
|
||||
then
|
||||
local defense = 100 - wesnoth.unit_defense(guardian, wesnoth.get_terrain(xa, ya))
|
||||
local defense = 100 - guardian:defense(wesnoth.get_terrain(xa, ya))
|
||||
local nh = AH.next_hop(guardian, xa, ya)
|
||||
if nh then
|
||||
if (nh[1] == xa) and (nh[2] == ya) and (defense > best_defense) then
|
||||
|
@ -64,7 +64,7 @@ function ca_zone_guardian:execution(cfg)
|
|||
|
||||
-- Go through all hexes the guardian can reach, find closest to target
|
||||
-- Cannot use next_hop here since target hex is occupied by enemy
|
||||
local min_dist, nh = 9e99
|
||||
local min_dist, nh = math.huge
|
||||
for _,hex in ipairs(reach) do
|
||||
-- Only consider unoccupied hexes
|
||||
local unit_in_way = wesnoth.get_unit(hex[1], hex[2])
|
||||
|
@ -90,12 +90,7 @@ function ca_zone_guardian:execution(cfg)
|
|||
newpos = { cfg.station_x, cfg.station_y }
|
||||
-- Otherwise choose one randomly from those given in filter_location
|
||||
else
|
||||
local width, height = wesnoth.get_map_size()
|
||||
local locs_map = LS.of_pairs(wesnoth.get_locations {
|
||||
x = '1-' .. width,
|
||||
y = '1-' .. height,
|
||||
{ "and", zone }
|
||||
})
|
||||
local locs_map = LS.of_pairs(AH.get_locations_no_borders(zone))
|
||||
|
||||
-- Check out which of those hexes the guardian can reach
|
||||
local reach_map = LS.of_pairs(wesnoth.find_reach(guardian))
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
local H = wesnoth.require "helper"
|
||||
local T = wml.tag
|
||||
local AH = wesnoth.require("ai/lua/ai_helper.lua")
|
||||
local MAIUV = wesnoth.require "ai/micro_ais/micro_ai_unit_variables.lua"
|
||||
|
||||
local micro_ai_helper = {}
|
||||
|
|
|
@ -1,86 +1,50 @@
|
|||
-- This set of functions provides a consistent way of storing Micro AI
|
||||
-- variables in units. They need to be stored inside a [micro_ai] tag in a
|
||||
-- unit's [variables] tag together with an ai_id= key, so that they can be
|
||||
-- removed when the Micro AI gets deleted. Otherwise subsequent Micro AIs used
|
||||
-- in the same scenario (or using the same units in later scenarios) might work
|
||||
-- incorrectly or not at all.
|
||||
-- Note that, with this method, there can only ever be one of these tags for each
|
||||
-- ai_ca in each unit (but of course several when there are several Micro AIs
|
||||
-- with different ai_CA values affecting the same unit)
|
||||
-- For the time being, we only allow key=value style variables.
|
||||
-- variables in units. Individual variables are stored inside a table with a
|
||||
-- name specific to the MAI ('micro_ai-' .. ai_id). This table is removed when
|
||||
-- the Micro AI is deleted in order to ensure that subsequent Micro AIs used
|
||||
-- in the same scenario (or using the same units in later scenarios) work
|
||||
-- correctly.
|
||||
-- Note that, with this method, there can only ever be one of these tables for each
|
||||
-- ai_id in each unit, but several tables are created for the same unit when there
|
||||
-- are several Micro AIs with different ai_id values.
|
||||
-- For the time being, we do not allow sub-tables. This is done because these
|
||||
-- unit variables are required to be persistent across save-load cycles and
|
||||
-- therefore need to be in WML table format. This could be extended to allow
|
||||
-- sub-tables in WML format, but there is no need for that at this time.
|
||||
|
||||
local micro_ai_unit_variables = {}
|
||||
|
||||
function micro_ai_unit_variables.modify_mai_unit_variables(unit, ai_id, action, vars_table)
|
||||
-- Modify [unit][variables][micro_ai] tags
|
||||
-- @ai_id (string): the id of the Micro AI
|
||||
-- @action (string): "delete", "set" or "insert"
|
||||
-- @vars_table: table of key=value pairs with the variables to be set or inserted (not needed for @action="delete")
|
||||
|
||||
local variables = unit.variables.__cfg
|
||||
|
||||
-- Always delete the respective [variables][micro_ai] tag, if it exists
|
||||
local existing_table
|
||||
for i,mai in ipairs(variables) do
|
||||
if (mai[1] == "micro_ai") and (mai[2].ai_id == ai_id) then
|
||||
existing_table = mai[2]
|
||||
table.remove(variables, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
-- Then replace it, if the "set" action is selected
|
||||
-- or add the new keys to it, overwriting old ones with the same name, if action == "insert"
|
||||
if (action == "set") or (action == "insert") then
|
||||
local tag = { "micro_ai" }
|
||||
|
||||
if (not existing_table) or (action == "set") then
|
||||
tag[2] = vars_table
|
||||
tag[2].ai_id = ai_id
|
||||
else
|
||||
for k,v in pairs(vars_table) do existing_table[k] = v end
|
||||
tag[2] = existing_table
|
||||
end
|
||||
|
||||
table.insert(variables, tag)
|
||||
end
|
||||
|
||||
-- All of this so far was only on the table dump -> apply to unit
|
||||
unit.variables.__cfg = variables
|
||||
end
|
||||
|
||||
function micro_ai_unit_variables.delete_mai_unit_variables(unit, ai_id)
|
||||
micro_ai_unit_variables.modify_mai_unit_variables(unit, ai_id, "delete")
|
||||
unit.variables['micro_ai_' .. ai_id] = nil
|
||||
end
|
||||
|
||||
function micro_ai_unit_variables.insert_mai_unit_variables(unit, ai_id, vars_table)
|
||||
micro_ai_unit_variables.modify_mai_unit_variables(unit, ai_id, "insert", vars_table)
|
||||
local mai_var = unit.variables['micro_ai_' .. ai_id] or {}
|
||||
-- Restrict to top-level named fields
|
||||
for k,v in pairs(vars_table) do mai_var[k] = v end
|
||||
unit.variables['micro_ai_' .. ai_id] = mai_var
|
||||
end
|
||||
|
||||
function micro_ai_unit_variables.set_mai_unit_variables(unit, ai_id, vars_table)
|
||||
micro_ai_unit_variables.modify_mai_unit_variables(unit, ai_id, "set", vars_table)
|
||||
local mai_var = {}
|
||||
-- Restrict to top-level named fields
|
||||
for k,v in pairs(vars_table) do mai_var[k] = v end
|
||||
unit.variables['micro_ai_' .. ai_id] = mai_var
|
||||
end
|
||||
|
||||
function micro_ai_unit_variables.get_mai_unit_variables(unit, ai_id, key)
|
||||
-- Get the content of [unit][variables][micro_ai] tag for the given @ai_id
|
||||
-- Get the content of [unit][variables]['micro_ai_' .. ai_id] tag
|
||||
-- Return value:
|
||||
-- - If tag is found: value of key if @key parameter is given, otherwise
|
||||
-- table of key=value pairs (including the ai_id key)
|
||||
-- - If no such tag is found: nil (if @key is set), otherwise empty table
|
||||
-- - If tag is found: value of key if @key parameter is given, otherwise entire table
|
||||
-- - If no such tag is found: nil if @key is given, otherwise empty table
|
||||
|
||||
for mai in wml.child_range(unit.variables.__cfg, "micro_ai") do
|
||||
if (mai.ai_id == ai_id) then
|
||||
if key then
|
||||
return mai[key]
|
||||
else
|
||||
return mai
|
||||
end
|
||||
end
|
||||
local mai_var = unit.variables['micro_ai_' .. ai_id] or {}
|
||||
|
||||
if key then
|
||||
return mai_var[key]
|
||||
else
|
||||
return mai_var
|
||||
end
|
||||
|
||||
-- If we got here, no corresponding tag was found
|
||||
-- Return empty table; or nil if @key was set
|
||||
if (not key) then return {} end
|
||||
end
|
||||
|
||||
return micro_ai_unit_variables
|
||||
|
|
|
@ -1,23 +1,18 @@
|
|||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local BC = wesnoth.require "ai/lua/battle_calcs.lua"
|
||||
--local LS = wesnoth.require "location_set"
|
||||
local M = wesnoth.map
|
||||
|
||||
local ca_ogres_flee = {}
|
||||
|
||||
function ca_ogres_flee:evaluation()
|
||||
local units = wesnoth.get_units { side = wesnoth.current.side,
|
||||
formula = 'movement_left > 0'
|
||||
}
|
||||
local units = AH.get_units_with_moves { side = wesnoth.current.side }
|
||||
|
||||
if (not units[1]) then return 0 end
|
||||
return 110000
|
||||
end
|
||||
|
||||
function ca_ogres_flee:execution()
|
||||
local units = wesnoth.get_units { side = wesnoth.current.side,
|
||||
formula = 'movement_left > 0'
|
||||
}
|
||||
local units = AH.get_units_with_moves { side = wesnoth.current.side }
|
||||
|
||||
local units_noMP = wesnoth.get_units { side = wesnoth.current.side,
|
||||
formula = 'movement_left = 0'
|
||||
|
@ -29,12 +24,9 @@ function ca_ogres_flee:execution()
|
|||
local enemies = wesnoth.get_units { { "filter_side", { {"enemy_of", {side = wesnoth.current.side} } } } }
|
||||
local enemy_attack_map = BC.get_attack_map(enemies)
|
||||
|
||||
local best_hex, best_unit, max_rating = {}, nil, -9e99
|
||||
local max_rating, best_hex, best_unit = - math.huge
|
||||
for i,u in ipairs(units) do
|
||||
local reach = wesnoth.find_reach(u)
|
||||
|
||||
--local rating_map = LS.create()
|
||||
|
||||
for j,r in ipairs(reach) do
|
||||
local unit_in_way = wesnoth.get_unit(r[1], r[2])
|
||||
|
||||
|
@ -74,8 +66,6 @@ function ca_ogres_flee:execution()
|
|||
|
||||
rating = rating + own_unit_rating * own_unit_weight
|
||||
|
||||
--rating_map:insert(r[1], r[2], rating)
|
||||
|
||||
if (rating > max_rating) then
|
||||
best_hex = { r[1], r[2] }
|
||||
best_unit = u
|
||||
|
@ -83,10 +73,7 @@ function ca_ogres_flee:execution()
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
--AH.put_labels(rating_map)
|
||||
end
|
||||
--print(best_unit.id, best_unit.x, best_unit.y, best_hex[1], best_hex[2], max_rating)
|
||||
|
||||
if best_hex then
|
||||
AH.movefull_outofway_stopunit(ai, best_unit, best_hex[1], best_hex[2])
|
||||
|
|
|
@ -40,7 +40,6 @@ function ca_transport:execution()
|
|||
then
|
||||
transport_map:insert(u.x, u.y)
|
||||
table.insert(transports, u)
|
||||
--print("----> Inserting " .. u.id, u.x, u.y, u.variables.destination_x, u.variables.destination_y)
|
||||
else
|
||||
blocked_hex_map:insert(u.x, u.y)
|
||||
end
|
||||
|
@ -54,7 +53,7 @@ function ca_transport:execution()
|
|||
}
|
||||
)
|
||||
|
||||
local max_rating, best_unit, best_hex, best_adj_tiles = -9e99
|
||||
local max_rating, best_unit, best_hex, best_adj_tiles = - math.huge
|
||||
for i,u in ipairs(transports) do
|
||||
local dst = { u.variables.destination_x, u.variables.destination_y }
|
||||
|
||||
|
@ -105,18 +104,18 @@ function ca_transport:execution()
|
|||
end
|
||||
end
|
||||
|
||||
if (max_rating > -9e99) then
|
||||
if best_unit then
|
||||
ai.move_full(best_unit, best_hex[1], best_hex[2])
|
||||
|
||||
-- Also unload units
|
||||
table.sort(best_adj_tiles, function(a, b) return a[3] > b[3] end)
|
||||
|
||||
local command_data = { x = best_unit.x, y = best_unit.y }
|
||||
for i = 1, math.min(#best_adj_tiles, 3) do
|
||||
table.insert(command_data, T.dst { x = best_adj_tiles[i][1], y = best_adj_tiles[i][2]} )
|
||||
end
|
||||
local command_data = { x = best_unit.x, y = best_unit.y }
|
||||
for i = 1, math.min(#best_adj_tiles, 3) do
|
||||
table.insert(command_data, T.dst { x = best_adj_tiles[i][1], y = best_adj_tiles[i][2]} )
|
||||
end
|
||||
|
||||
wesnoth.invoke_synced_command("ship_unload", command_data)
|
||||
wesnoth.invoke_synced_command("ship_unload", command_data)
|
||||
|
||||
return
|
||||
end
|
||||
|
@ -129,12 +128,12 @@ function ca_transport:execution()
|
|||
}
|
||||
)
|
||||
|
||||
local max_rating, best_unit, best_hex = -9e99, {}, {}
|
||||
local max_rating, best_unit, best_hex = - math.huge
|
||||
for i,u in ipairs(transports) do
|
||||
local dst = { u.variables.destination_x, u.variables.destination_y }
|
||||
local reach = wesnoth.find_reach(u)
|
||||
|
||||
local max_rating_unit, best_hex_unit = -9e99, {}
|
||||
local max_rating_unit, best_hex_unit = - math.huge
|
||||
for i,r in ipairs(reach) do
|
||||
if deep_water_map:get(r[1], r[2]) and (not blocked_hex_map:get(r[1], r[2])) then
|
||||
local rating = -M.distance_between(r[1], r[2], dst[1], dst[2])
|
||||
|
@ -150,7 +149,7 @@ function ca_transport:execution()
|
|||
|
||||
-- We give a penalty to hexes occupied by another transport that can still move away.
|
||||
-- All ratings need to be set to the same value for this to work.
|
||||
if (max_rating_unit > -9e99) then
|
||||
if best_hex_unit then
|
||||
max_rating_unit = 0
|
||||
if transport_map:get(best_hex_unit[1], best_hex_unit[2]) then
|
||||
max_rating_unit = -1
|
||||
|
@ -164,7 +163,7 @@ function ca_transport:execution()
|
|||
end
|
||||
end
|
||||
|
||||
if best_unit.id then
|
||||
if best_unit then
|
||||
ai.move_full(best_unit, best_hex[1], best_hex[2])
|
||||
else -- still need to make sure gamestate gets changed
|
||||
ai.stopunit_moves(transports[1])
|
||||
|
|
|
@ -1,28 +1,25 @@
|
|||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
local AANS_attack
|
||||
|
||||
local ca_aggressive_attack_no_suicide = {}
|
||||
|
||||
function ca_aggressive_attack_no_suicide:evaluation(cfg, data)
|
||||
|
||||
local units = wesnoth.get_units {
|
||||
side = wesnoth.current.side,
|
||||
formula = 'attacks_left > 0'
|
||||
}
|
||||
--print('#units', #units)
|
||||
local units = AH.get_units_with_attacks { side = wesnoth.current.side }
|
||||
if (not units[1]) then return 0 end
|
||||
|
||||
-- Get all possible attacks
|
||||
local attacks = AH.get_attacks(units, { include_occupied = true })
|
||||
--print('#attacks', #attacks)
|
||||
if (not attacks[1]) then return 0 end
|
||||
|
||||
-- Now find the best of the possible attacks
|
||||
local max_rating, best_attack = -9e99, {}
|
||||
local max_rating, best_attack = - math.huge
|
||||
for i, att in ipairs(attacks) do
|
||||
local attacker = wesnoth.get_unit(att.src.x, att.src.y)
|
||||
local defender = wesnoth.get_unit(att.target.x, att.target.y)
|
||||
|
||||
local attacker_dst = wesnoth.copy_unit(attacker)
|
||||
local attacker_dst = attacker:clone()
|
||||
attacker_dst.x, attacker_dst.y = att.dst.x, att.dst.y
|
||||
|
||||
local att_stats, def_stats = wesnoth.simulate_combat(attacker_dst, defender)
|
||||
|
@ -38,7 +35,6 @@ function ca_aggressive_attack_no_suicide:evaluation(cfg, data)
|
|||
|
||||
-- Also, take strongest unit first
|
||||
rating = rating + attacker.hitpoints / 10.
|
||||
--print('rating:', rating, attacker.id, defender.id)
|
||||
|
||||
if (rating > max_rating) then
|
||||
max_rating = rating
|
||||
|
@ -47,8 +43,8 @@ function ca_aggressive_attack_no_suicide:evaluation(cfg, data)
|
|||
end
|
||||
end
|
||||
|
||||
if (max_rating > -9e99) then
|
||||
data.attack = best_attack
|
||||
if best_attack then
|
||||
AANS_attack = best_attack
|
||||
return 100000
|
||||
end
|
||||
|
||||
|
@ -56,8 +52,8 @@ function ca_aggressive_attack_no_suicide:evaluation(cfg, data)
|
|||
end
|
||||
|
||||
function ca_aggressive_attack_no_suicide:execution(cfg, data)
|
||||
AH.robust_move_and_attack(ai, data.attack.src, data.attack.dst, data.attack.target)
|
||||
data.attack = nil
|
||||
AH.robust_move_and_attack(ai, AANS_attack.src, AANS_attack.dst, AANS_attack.target)
|
||||
AANS_attack = nil
|
||||
end
|
||||
|
||||
return ca_aggressive_attack_no_suicide
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
local R = wesnoth.require "ai/lua/retreat.lua"
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
local retreat_unit, retreat_dst
|
||||
|
||||
local retreat = {}
|
||||
|
||||
function retreat:evaluation(cfg, data)
|
||||
|
||||
local units = wesnoth.get_units {
|
||||
side = wesnoth.current.side,
|
||||
formula = 'movement_left > 0'
|
||||
}
|
||||
--print('#units', #units)
|
||||
local units = AH.get_units_with_moves { side = wesnoth.current.side }
|
||||
if (not units[1]) then return 0 end
|
||||
|
||||
local unit, dst, enemy_threat = R.retreat_injured_units(units)
|
||||
|
||||
if unit then
|
||||
data.retreat_unit = unit
|
||||
data.retreat_dst = dst
|
||||
retreat_unit = unit
|
||||
retreat_dst = dst
|
||||
return 101000
|
||||
end
|
||||
|
||||
|
@ -24,8 +22,8 @@ function retreat:evaluation(cfg, data)
|
|||
end
|
||||
|
||||
function retreat:execution(cfg, data)
|
||||
AH.movefull_outofway_stopunit(ai, data.retreat_unit, data.retreat_dst[1], data.retreat_dst[2])
|
||||
data.retreat_unit, data.retreat_dst = nil, nil
|
||||
AH.movefull_outofway_stopunit(ai, retreat_unit, retreat_dst[1], retreat_dst[2])
|
||||
retreat_unit, retreat_dst = nil, nil
|
||||
end
|
||||
|
||||
return retreat
|
||||
|
|
|
@ -28,10 +28,10 @@ function muff_toras_move:execution()
|
|||
local rating = -10000 -- This is the base rating if no other units are left
|
||||
|
||||
-- Main rating is distance from the closest own unit
|
||||
local min_dist
|
||||
local min_dist = math.huge
|
||||
for _,unit in ipairs(units) do
|
||||
local dist = M.distance_between(x, y, unit.x, unit.y)
|
||||
if (not min_dist) or (dist < min_dist) then
|
||||
if (dist < min_dist) then
|
||||
min_dist = dist
|
||||
end
|
||||
end
|
||||
|
@ -64,8 +64,6 @@ function muff_toras_move:execution()
|
|||
|
||||
if ((go_to[1] ~= muff_toras.x) or (go_to[2] ~= muff_toras.y)) then
|
||||
AH.robust_move_and_attack(ai, muff_toras, go_to)
|
||||
else
|
||||
AH.checked_stopunit_moves(ai, muff_toras)
|
||||
end
|
||||
|
||||
-- Test whether an attack without retaliation or with little damage is possible
|
||||
|
@ -75,12 +73,12 @@ function muff_toras_move:execution()
|
|||
|
||||
local targets = AH.get_attackable_enemies { { "filter_adjacent", { id = muff_toras.id } } }
|
||||
|
||||
local max_rating, best_target, best_weapon = -9e99
|
||||
local max_rating, best_target, best_weapon = - math.huge
|
||||
for _,target in ipairs(targets) do
|
||||
for n_weapon,weapon in ipairs(muff_toras.attacks) do
|
||||
local att_stats, def_stats = wesnoth.simulate_combat(muff_toras, n_weapon, target)
|
||||
|
||||
local rating = -9e99
|
||||
local rating = - math.huge
|
||||
-- This is an acceptable attack if:
|
||||
-- 1. There is no counter attack
|
||||
-- 2. Probability of death is >=67% for enemy, 0% for attacker (default values)
|
|
@ -429,10 +429,10 @@ Besides... I want my brother back."
|
|||
{MODIFY_AI_ADD_CANDIDATE_ACTION 3 main_loop (
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=muff_toras_move
|
||||
id=muff_toras_move
|
||||
name=ca_muff_toras_move
|
||||
id=ca_muff_toras_move
|
||||
max_score=15000
|
||||
location="campaigns/Two_Brothers/lua/muff_toras_move.lua"
|
||||
location="campaigns/Two_Brothers/ai/ca_muff_toras_move.lua"
|
||||
[/candidate_action]
|
||||
)}
|
||||
|
||||
|
|
|
@ -537,15 +537,6 @@
|
|||
# move-to-enemy candidate actions.
|
||||
# Put this into the [side][ai] tag.
|
||||
# Does not work in [modify_side][ai] or [modify_ai] at the moment.
|
||||
[engine]
|
||||
name="lua"
|
||||
code= <<
|
||||
local _,data = ...
|
||||
local exp_ai = wesnoth.require("ai/lua/generic_rush_engine.lua").init(ai)
|
||||
exp_ai.data = data
|
||||
return exp_ai
|
||||
>>
|
||||
[/engine]
|
||||
[stage]
|
||||
id=main_loop
|
||||
name=ai_default_rca::candidate_action_evaluation_loop
|
||||
|
@ -564,57 +555,49 @@
|
|||
engine=lua
|
||||
name=recruit_rushers
|
||||
max_score=300000
|
||||
evaluation="return (...):recruit_rushers_eval()"
|
||||
execution="(...):recruit_rushers_exec()"
|
||||
location="ai/lua/ca_recruit_rushers.lua"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=switch_castle
|
||||
max_score=290000
|
||||
evaluation="return (...):castle_switch_eval()"
|
||||
execution="(...):castle_switch_exec()"
|
||||
location="ai/lua/ca_castle_switch.lua"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=retreat_injured
|
||||
max_score=205000
|
||||
evaluation="return (...):retreat_injured_units_eval()"
|
||||
execution="(...):retreat_injured_units_exec()"
|
||||
location="ai/lua/ca_retreat_injured.lua"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=grab_villages
|
||||
max_score=200000
|
||||
evaluation="return (...):grab_villages_eval()"
|
||||
execution="(...):grab_villages_exec()"
|
||||
location="ai/lua/ca_grab_villages.lua"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=spread_poison
|
||||
max_score=190000
|
||||
evaluation="return (...):spread_poison_eval()"
|
||||
execution="(...):spread_poison_exec()"
|
||||
location="ai/lua/ca_spread_poison.lua"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=place_healers
|
||||
max_score=95000
|
||||
evaluation="return (...):place_healers_eval()"
|
||||
execution="(...):place_healers_exec()"
|
||||
location="ai/lua/ca_place_healers.lua"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=hunt_villages
|
||||
name=village_hunt
|
||||
max_score=30000
|
||||
evaluation="return (...):village_hunt_eval()"
|
||||
execution="(...):village_hunt_exec()"
|
||||
location="ai/lua/ca_village_hunt.lua"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=move_to_enemy
|
||||
name=move_to_any_enemy
|
||||
max_score=1
|
||||
evaluation="return (...):move_to_enemy_eval()"
|
||||
execution="(...):move_to_enemy_exec()"
|
||||
location="ai/lua/ca_move_to_any_enemy.lua"
|
||||
[/candidate_action]
|
||||
[/stage]
|
||||
#enddef
|
||||
|
|
Loading…
Add table
Reference in a new issue