Merge pull request #3506 from mattsc/lua_ai_cleanup

Lua AI Cleanup
This commit is contained in:
mattsc 2018-09-22 06:18:33 -07:00 committed by GitHub
commit 81ef9ea390
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 1138 additions and 1248 deletions

View file

@ -1,2 +1,2 @@
# wmllint: no translatables
{ai/ais/ai_generic_rush.cfg}
{ai/ais/ai_experimental.cfg}

View file

@ -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]

View file

@ -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

View 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

View 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

View file

@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View file

@ -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})

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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")

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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])

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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))

View file

@ -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 = {}

View file

@ -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

View file

@ -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])

View file

@ -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])

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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]
)}

View file

@ -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