Add new AI
This commit is contained in:
parent
1c8fb5a3ac
commit
6830fc1719
7 changed files with 2494 additions and 0 deletions
|
@ -26,6 +26,10 @@ Another improvement is the new option system. UMC authors now can have their add
|
|||
With a lot of new possibilities for conflicts between add-ons, there's now a dependency system for eras, scenarios and modifications, not to be confused with the already existing dependency management for whole add-ons. See the appopriate sections of [wiki]EraWML[/wiki], [wiki]ScenarioWML[/wiki] and [wiki]ModificationWML[/wiki] for details.
|
||||
[/section]
|
||||
|
||||
[section="New AI"]
|
||||
There is a new AI available in multiplayer named "Experimental AI". Boasting an 86% win rate over the current RCA AI, it features several improvements over the current default AI, including village grabbing, castle selection, targeted poisoning, and recruitment.
|
||||
[/section]
|
||||
|
||||
[section="Another Change"]
|
||||
[/section]
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
Version 1.11.0+svn:
|
||||
* AI:
|
||||
* New AI: Experimental AI
|
||||
* Improved recruitment, castle and village management over current default AI
|
||||
* Campaigns:
|
||||
* Dead Water:
|
||||
* Removed duplicated loyalty overlay (that now is in core), and used
|
||||
|
|
65
data/ai/ais/ai_generic-rush.cfg
Normal file
65
data/ai/ais/ai_generic-rush.cfg
Normal file
|
@ -0,0 +1,65 @@
|
|||
#textdomain wesnoth
|
||||
|
||||
{core/macros/ai_candidate_actions.cfg}
|
||||
|
||||
[ai]
|
||||
id=experimental_ai
|
||||
description="Experimental AI"
|
||||
version=10710
|
||||
[engine]
|
||||
name="lua"
|
||||
code= <<
|
||||
local ai = ...
|
||||
return wesnoth.require("ai/lua/generic-rush_engine.lua").init(ai)
|
||||
>>
|
||||
[/engine]
|
||||
[stage]
|
||||
id=main_loop
|
||||
name=testing_ai_default::candidate_action_evaluation_loop
|
||||
{AI_CA_GOTO}
|
||||
#{AI_CA_RECRUITMENT}
|
||||
{AI_CA_MOVE_LEADER_TO_GOALS}
|
||||
{AI_CA_MOVE_LEADER_TO_KEEP}
|
||||
{AI_CA_COMBAT}
|
||||
{AI_CA_HEALING}
|
||||
{AI_CA_VILLAGES}
|
||||
{AI_CA_RETREAT}
|
||||
{AI_CA_MOVE_TO_TARGETS}
|
||||
{AI_CA_PASSIVE_LEADER_SHARES_KEEP}
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=stats
|
||||
max_score=999999
|
||||
evaluation="return (...):stats_eval()"
|
||||
execution="(...):stats_exec()"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=recruit_rushers
|
||||
max_score=300000
|
||||
evaluation="return (...):recruit_rushers_eval()"
|
||||
execution="(...):recruit_rushers_exec()"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=switch_castle
|
||||
max_score=290000
|
||||
evaluation="return (...):castle_switch_eval()"
|
||||
execution="(...):castle_switch_exec()"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=grab_villages
|
||||
max_score=200000
|
||||
evaluation="return (...):grab_villages_eval()"
|
||||
execution="(...):grab_villages_exec()"
|
||||
[/candidate_action]
|
||||
[candidate_action]
|
||||
engine=lua
|
||||
name=spread_poison
|
||||
max_score=190000
|
||||
evaluation="return (...):spread_poison_eval()"
|
||||
execution="(...):spread_poison_exec()"
|
||||
[/candidate_action]
|
||||
[/stage]
|
||||
[/ai]
|
1271
data/ai/lua/ai_helper.lua
Normal file
1271
data/ai/lua/ai_helper.lua
Normal file
File diff suppressed because it is too large
Load diff
762
data/ai/lua/generic-recruit_engine.lua
Executable file
762
data/ai/lua/generic-recruit_engine.lua
Executable file
|
@ -0,0 +1,762 @@
|
|||
return {
|
||||
-- init parameters:
|
||||
-- ai: a reference to the ai engine so recruit has access to ai functions
|
||||
-- ai_cas: an object reference to store the CAs and associated data
|
||||
-- the CA will use the function names ai_cas:recruit_rushers_eval/exec, so should be referenced by the object name used by the calling AI
|
||||
-- score_function: a function that returns the CA score when recruit_rushers_eval wants to recruit
|
||||
init = function(ai, ai_cas, score_function)
|
||||
-- default score function if one not provided
|
||||
if not score_function then
|
||||
score_function = function() return 300000 end
|
||||
end
|
||||
|
||||
local H = wesnoth.require "lua/helper.lua"
|
||||
local W = H.set_wml_action_metatable {}
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
local get_next_id = (function()
|
||||
local next_id = 0
|
||||
return function()
|
||||
next_id = next_id + 1
|
||||
return next_id
|
||||
end
|
||||
end)()
|
||||
|
||||
function living(unit)
|
||||
return not unit.status.not_living
|
||||
end
|
||||
|
||||
function get_best_defense(unit)
|
||||
local terrain_archetypes = { "Wo", "Ww", "Wwr", "Ss", "Gt", "Ds", "Ft", "Hh", "Mm", "Vi", "Ch", "Uu", "At", "Qt", "^Uf", "Xt" }
|
||||
local best_defense = 100
|
||||
|
||||
for i, terrain in ipairs(terrain_archetypes) do
|
||||
local defense = wesnoth.unit_defense(unit, terrain)
|
||||
if defense < best_defense then
|
||||
best_defense = defense
|
||||
end
|
||||
end
|
||||
|
||||
return best_defense
|
||||
end
|
||||
|
||||
function ai_cas:analyze_enemy_unit(unit_type_id)
|
||||
local function get_best_attack(attacker, defender, unit_defense, can_poison)
|
||||
-- Try to find the average damage for each possible attack and return the one that deals the most damage.
|
||||
-- Would be preferable to call simulate combat, but that requires the defender to be on the map according
|
||||
-- to documentation and we are looking for hypothetical situations so would have to search for available
|
||||
-- locations for the defender that would have the desired defense. We would also need to remove nearby units
|
||||
-- in order to ensure that adjacent units are not modifying the result. In addition, the time of day is
|
||||
-- assumed to be neutral here, which is not assured in the simulation.
|
||||
-- Ideally, this function would be a clone of simulate combat, but run for each time of day in the scenario and on arbitrary terrain.
|
||||
-- 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
|
||||
-- This doesn't actually check for the ability steadfast, but gives correct answer in the default era
|
||||
-- TODO: find a more reliable method
|
||||
local steadfast = false -- wesnoth.unit_ability(defender, "resistance")
|
||||
|
||||
for attack in H.child_range(wesnoth.unit_types[attacker.type].__cfg, "attack") do
|
||||
local defense = unit_defense
|
||||
local poison = false
|
||||
local damage_multiplier = 1
|
||||
-- TODO: handle more abilities (charge, drain)
|
||||
for special in H.child_range(attack, 'specials') do
|
||||
local mod
|
||||
if H.get_child(special, 'poison') and can_poison then
|
||||
poison = true
|
||||
end
|
||||
|
||||
-- Handle marksman and magical
|
||||
-- TODO: Make this work properly for UMC chance_to_hit (does not account for all keys)
|
||||
mod = H.get_child(special, 'chance_to_hit')
|
||||
if mod then
|
||||
if mod.cumulative then
|
||||
if mod.value > defense then
|
||||
defense = mod.value
|
||||
end
|
||||
else
|
||||
defense = mod.value
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle backstab
|
||||
mod = H.get_child(special, 'damage')
|
||||
if mod then
|
||||
if mod.backstab then
|
||||
-- Assume backstab happens on only 1/2 of attacks
|
||||
-- TODO: find out what actual probability is
|
||||
damage_multiplier = damage_multiplier*(mod.multiply*0.5 + 0.5)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Handle drain for defender
|
||||
local drain_recovery = 0
|
||||
for defender_attack in H.child_range(defender.__cfg, 'attack') do
|
||||
if (defender_attack.range == attack.range) then
|
||||
for special in H.child_range(defender_attack, 'specials') do
|
||||
if H.get_child(special, 'drains') and living(attacker) then
|
||||
-- TODO: handle chance to hit
|
||||
-- currently assumes 50% chance to hit using supplied constant
|
||||
local attacker_resistance = wesnoth.unit_resistance(attacker, defender_attack.type)
|
||||
drain_recovery = (defender_attack.damage*defender_attack.number*attacker_resistance*0.25)/100
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defense = defense/100.0
|
||||
local resistance = wesnoth.unit_resistance(defender, attack.type)
|
||||
if steadfast and (resistance < 100) then
|
||||
resistance = 100 - ((100 - resistance) * 2)
|
||||
if (resistance < 50) then
|
||||
resistance = 50
|
||||
end
|
||||
end
|
||||
local base_damage = attack.damage*resistance*damage_multiplier
|
||||
if (resistance > 100) then
|
||||
base_damage = base_damage-1
|
||||
end
|
||||
base_damage = math.floor(base_damage/100 + 0.5)
|
||||
if (base_damage < 1) and (attack.damage > 0) then
|
||||
-- Damage is always at least 1
|
||||
base_damage = 1
|
||||
end
|
||||
local attack_damage = base_damage*attack.number*defense-drain_recovery
|
||||
|
||||
local poison_damage = 0
|
||||
if poison then
|
||||
-- Add poison damage * probability of poisoning
|
||||
poison_damage = 8*(1-((1-defense)^attack.number))
|
||||
attack_damage = attack_damage + poison_damage
|
||||
end
|
||||
|
||||
if (not best_attack) or (attack_damage > best_damage) then
|
||||
best_damage = attack_damage
|
||||
best_attack = attack
|
||||
end
|
||||
end
|
||||
|
||||
return best_attack, best_damage, poison_damage
|
||||
end
|
||||
|
||||
-- Use cached information when possible: this is expensive
|
||||
-- TODO: Invalidate cache when recruit list changes
|
||||
if not self.data.analyses then
|
||||
self.data.analyses = {}
|
||||
else
|
||||
if self.data.analyses[unit_type_id] then
|
||||
return self.data.analyses[unit_type_id]
|
||||
end
|
||||
end
|
||||
|
||||
local analysis = {}
|
||||
|
||||
local unit = wesnoth.create_unit {
|
||||
type = unit_type_id,
|
||||
random_traits = false,
|
||||
name = "X",
|
||||
id = unit_type_id .. get_next_id(),
|
||||
random_gender = false
|
||||
}
|
||||
local can_poison = living(unit) or wesnoth.unit_ability(unit, 'regenerate')
|
||||
local flat_defense = wesnoth.unit_defense(unit, "Gt")
|
||||
local best_defense = get_best_defense(unit)
|
||||
|
||||
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
local recruit = wesnoth.create_unit {
|
||||
type = recruit_id,
|
||||
random_traits = false,
|
||||
name = "X",
|
||||
id = recruit_id .. get_next_id(),
|
||||
random_gender = false
|
||||
}
|
||||
local can_poison_retaliation = living(recruit) or wesnoth.unit_ability(recruit, 'regenerate')
|
||||
best_flat_attack, best_flat_damage, flat_poison = get_best_attack(recruit, unit, flat_defense, can_poison)
|
||||
best_high_defense_attack, best_high_defense_damage, high_defense_poison = get_best_attack(recruit, unit, best_defense, can_poison)
|
||||
best_retaliation, best_retaliation_damage, retaliation_poison = get_best_attack(unit, recruit, wesnoth.unit_defense(recruit, "Gt"), can_poison_retaliation)
|
||||
|
||||
local result = {
|
||||
offense = { attack = best_flat_attack, damage = best_flat_damage, poison_damage = flat_poison },
|
||||
defense = { attack = best_high_defense_attack, damage = best_high_defense_damage, poison_damage = high_defense_poison },
|
||||
retaliation = { attack = best_retaliation, damage = best_retaliation_damage, poison_damage = retaliation_poison }
|
||||
}
|
||||
analysis[recruit_id] = result
|
||||
end
|
||||
|
||||
-- Cache result before returning
|
||||
self.data.analyses[unit_type_id] = analysis
|
||||
return analysis
|
||||
end
|
||||
|
||||
function get_hp_efficiency()
|
||||
local efficiency = {}
|
||||
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
-- raw durability is a function of defense and hp
|
||||
-- efficiency decreases faster than cost increases to avoid recruiting many expensive units
|
||||
-- there is a requirement for bodies in order to block movement
|
||||
efficiency[recruit_id] = math.max(math.log(wesnoth.unit_types[recruit_id].max_hitpoints/20),0.01)/(wesnoth.unit_types[recruit_id].cost^2)
|
||||
end
|
||||
return efficiency
|
||||
end
|
||||
|
||||
function can_slow(unit)
|
||||
for defender_attack in H.child_range(unit.__cfg, 'attack') do
|
||||
for special in H.child_range(defender_attack, 'specials') do
|
||||
if H.get_child(special, 'slow') then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function get_hp_ratio_with_gold()
|
||||
-- Hitpoint ratio of own units / enemy units
|
||||
-- Also convert available gold to a hp estimate
|
||||
my_units = AH.get_live_units {
|
||||
{ "filter_side", {{"allied_with", {side = wesnoth.current.side} }} }
|
||||
}
|
||||
enemies = AH.get_live_units {
|
||||
{ "filter_side", {{"enemy_of", {side = wesnoth.current.side} }} }
|
||||
}
|
||||
|
||||
local my_hp, enemy_hp = 0, 0
|
||||
for i,u in ipairs(my_units) do my_hp = my_hp + u.hitpoints end
|
||||
for i,u in ipairs(enemies) do enemy_hp = enemy_hp + u.hitpoints end
|
||||
|
||||
my_hp = my_hp + wesnoth.sides[wesnoth.current.side].gold*2.3
|
||||
local enemy_gold = 0
|
||||
local enemies = wesnoth.get_sides {{"enemy_of", {side = wesnoth.current.side} }}
|
||||
for i,s in ipairs(enemies) do
|
||||
enemy_gold = enemy_gold + s.gold
|
||||
end
|
||||
enemy_hp = enemy_hp+enemy_gold*2.3
|
||||
hp_ratio = my_hp/(enemy_hp + 1e-6)
|
||||
|
||||
return hp_ratio
|
||||
end
|
||||
|
||||
function do_recruit_eval(data)
|
||||
-- Check if leader is on keep
|
||||
local leader = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[1]
|
||||
|
||||
if (not leader) or (not wesnoth.get_terrain_info(wesnoth.get_terrain(leader.x, leader.y)).keep) then
|
||||
return 0
|
||||
end
|
||||
|
||||
-- Check if there is enough gold to recruit a unit
|
||||
local cheapest_unit_cost = AH.get_cheapest_recruit_cost()
|
||||
if cheapest_unit_cost > wesnoth.sides[wesnoth.current.side].gold then
|
||||
return 0
|
||||
end
|
||||
|
||||
get_current_castle(leader, data)
|
||||
local no_space = true
|
||||
for i,c in ipairs(data.castle.locs) do
|
||||
local unit = wesnoth.get_unit(c[1], c[2])
|
||||
if (not unit) then
|
||||
no_space = false
|
||||
break
|
||||
end
|
||||
end
|
||||
if no_space then
|
||||
return 0
|
||||
end
|
||||
|
||||
if data.recruit == nil then
|
||||
data.recruit = init_data(leader)
|
||||
end
|
||||
data.recruit.cheapest_unit_cost = cheapest_unit_cost
|
||||
return score_function()
|
||||
end
|
||||
|
||||
function init_data(leader)
|
||||
local data = {}
|
||||
data.hp_efficiency = get_hp_efficiency()
|
||||
|
||||
-- Count enemies of each type
|
||||
local enemies = AH.get_live_units {
|
||||
{ "filter_side", {{"enemy_of", {side = wesnoth.current.side} }}}
|
||||
}
|
||||
local enemy_counts = {}
|
||||
local enemy_types = {}
|
||||
|
||||
local function add_unit_type(unit_type)
|
||||
if enemy_counts[unit_type] == nil then
|
||||
table.insert(enemy_types, unit_type)
|
||||
enemy_counts[unit_type] = 1
|
||||
else
|
||||
enemy_counts[unit_type] = enemy_counts[unit_type] + 1
|
||||
end
|
||||
end
|
||||
|
||||
-- Collect all enemies on map
|
||||
for i, unit in ipairs(enemies) do
|
||||
add_unit_type(unit.type)
|
||||
end
|
||||
-- Collect all possible enemy recruits and count them as virtual enemies
|
||||
local enemy_sides = wesnoth.get_sides({
|
||||
{ "enemy_of", {side = wesnoth.current.side} },
|
||||
{ "has_unit", { canrecruit = true }} })
|
||||
for i, side in ipairs(enemy_sides) do
|
||||
for j, unit_type in ipairs(wesnoth.sides[side.side].recruit) do
|
||||
add_unit_type(unit_type)
|
||||
end
|
||||
end
|
||||
data.enemy_counts = enemy_counts
|
||||
data.enemy_types = enemy_types
|
||||
data.num_enemies = #enemies
|
||||
|
||||
return data
|
||||
end
|
||||
|
||||
function ai_cas:recruit_rushers_eval()
|
||||
local start_time, ca_name = os.clock(), 'recruit_rushers'
|
||||
if AH.print_eval() then print(' - Evaluating recruit_rushers CA:', os.clock()) end
|
||||
|
||||
local score = do_recruit_eval(self.data)
|
||||
if score == 0 then
|
||||
-- We're done for the turn, discard data
|
||||
self.data.recruit = nil
|
||||
end
|
||||
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return score
|
||||
end
|
||||
|
||||
function ai_cas:recruit_rushers_exec()
|
||||
if AH.print_exec() then print(' ' .. os.clock() .. ' Executing recruit_rushers CA') end
|
||||
if AH.show_messages() then W.message { speaker = 'narrator', message = 'Recruiting' } end
|
||||
|
||||
local enemy_counts = self.data.recruit.enemy_counts
|
||||
local enemy_types = self.data.recruit.enemy_types
|
||||
local num_enemies = self.data.recruit.num_enemies
|
||||
local hp_ratio = get_hp_ratio_with_gold()
|
||||
|
||||
-- Determine effectiveness of recruitable units against each enemy unit type
|
||||
local recruit_effectiveness = {}
|
||||
local recruit_vulnerability = {}
|
||||
local attack_type_count = {} -- The number of units who will likely use a given attack type
|
||||
local attack_range_count = {} -- The number of units who will likely use a given attack range
|
||||
local unit_attack_type_count = {} -- The attack types a unit will use
|
||||
local unit_attack_range_count = {} -- The ranges a unit will use
|
||||
local enemy_type_count = 0
|
||||
for i, unit_type in ipairs(enemy_types) do
|
||||
local analysis = ai_cas:analyze_enemy_unit(unit_type)
|
||||
enemy_type_count = enemy_type_count + 1
|
||||
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
-- This line should be moved out of the loop!
|
||||
local recruit_count = #(AH.get_live_units { side = wesnoth.current.side, type = recruit_id, canrecruit = 'no' })
|
||||
|
||||
if recruit_effectiveness[recruit_id] == nil then
|
||||
recruit_effectiveness[recruit_id] = 0
|
||||
recruit_vulnerability[recruit_id] = 0
|
||||
end
|
||||
recruit_effectiveness[recruit_id] = recruit_effectiveness[recruit_id] + analysis[recruit_id].defense.damage * enemy_counts[unit_type]^2
|
||||
recruit_vulnerability[recruit_id] = recruit_vulnerability[recruit_id] + (analysis[recruit_id].retaliation.damage * enemy_counts[unit_type])^3
|
||||
|
||||
local attack_type = analysis[recruit_id].defense.attack.type
|
||||
if attack_type_count[attack_type] == nil then
|
||||
attack_type_count[attack_type] = 0
|
||||
end
|
||||
attack_type_count[attack_type] = attack_type_count[attack_type] + recruit_count
|
||||
|
||||
local attack_range = analysis[recruit_id].defense.attack.range
|
||||
if attack_range_count[attack_range] == nil then
|
||||
attack_range_count[attack_range] = 0
|
||||
end
|
||||
attack_range_count[attack_range] = attack_range_count[attack_range] + recruit_count
|
||||
|
||||
if unit_attack_type_count[recruit_id] == nil 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
|
||||
unit_attack_range_count[recruit_id] = {}
|
||||
end
|
||||
unit_attack_range_count[recruit_id][attack_range] = true
|
||||
end
|
||||
end
|
||||
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
recruit_effectiveness[recruit_id] = (recruit_effectiveness[recruit_id] / (num_enemies)^2)^0.5
|
||||
recruit_vulnerability[recruit_id] = (recruit_vulnerability[recruit_id] / ((num_enemies)^2))^0.5
|
||||
end
|
||||
-- Correct count of units for each range
|
||||
local most_common_range = nil
|
||||
local most_common_range_count = 0
|
||||
for range, count in pairs(attack_range_count) do
|
||||
attack_range_count[range] = count/enemy_type_count
|
||||
if attack_range_count[range] > most_common_range_count then
|
||||
most_common_range = range
|
||||
most_common_range_count = attack_range_count[range]
|
||||
end
|
||||
end
|
||||
-- Correct count of units for each attack type
|
||||
for attack_type, count in pairs(attack_type_count) do
|
||||
attack_type_count[attack_type] = count/enemy_type_count
|
||||
end
|
||||
|
||||
local recruit_type = nil
|
||||
local leader = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[1]
|
||||
repeat
|
||||
self.data.recruit.best_hex, self.data.recruit.target_hex = ai_cas:find_best_recruit_hex(leader, self.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
|
||||
|
||||
if wesnoth.unit_types[recruit_type].cost <= wesnoth.sides[wesnoth.current.side].gold then
|
||||
ai.recruit(recruit_type, self.data.recruit.best_hex[1], self.data.recruit.best_hex[2])
|
||||
return true
|
||||
else
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
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,
|
||||
{ "and", {
|
||||
x = leader.x, y = leader.y, radius = 200,
|
||||
{ "filter_radius", { terrain = 'C*^*,K*^*,*^Kov,*^Cov' } }
|
||||
}}
|
||||
},
|
||||
x = leader.x,
|
||||
y = leader.y
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
function ai_cas:find_best_recruit_hex(leader, data)
|
||||
-- Find the best recruit hex
|
||||
-- First choice: a hex that can reach an unowned village
|
||||
-- Second choice: a hex close to the enemy
|
||||
local enemy_leaders = AH.get_live_units { canrecruit = 'yes',
|
||||
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
|
||||
}
|
||||
local closest_enemy_distance, closest_enemy_location = AH.get_closest_enemy()
|
||||
|
||||
get_current_castle(leader, data)
|
||||
|
||||
local best_hex, village = get_village_target(leader, data)
|
||||
if not village[1] then
|
||||
-- no available village, look for hex closest to enemy leader
|
||||
local max_rating = -1
|
||||
for i,c in ipairs(data.castle.locs) do
|
||||
local rating = 0
|
||||
local unit = wesnoth.get_unit(c[1], c[2])
|
||||
if (not unit) then
|
||||
for j,e in ipairs(enemy_leaders) do
|
||||
rating = rating + 1 / H.distance_between(c[1], c[2], e.x, e.y) ^ 2.
|
||||
end
|
||||
rating = rating + 1 / H.distance_between(c[1], c[2], closest_enemy_location.x, closest_enemy_location.y) ^ 2.
|
||||
if (rating > max_rating) then
|
||||
max_rating, best_hex = rating, { c[1], c[2] }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if AH.print_exec() then
|
||||
if village[1] then
|
||||
print("Recruit at: " .. best_hex[1] .. "," .. best_hex[2] .. " -> " .. village[1] .. "," .. village[2])
|
||||
else
|
||||
print("Recruit at: " .. best_hex[1] .. "," .. best_hex[2])
|
||||
end
|
||||
end
|
||||
return best_hex, village
|
||||
end
|
||||
|
||||
function 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)
|
||||
-- Find best recruit based on damage done to enemies present, speed, and hp/gold ratio
|
||||
local recruit_scores = {}
|
||||
local best_scores = {offense = 0, defense = 0, move = 0}
|
||||
local best_hex = self.data.recruit.best_hex
|
||||
local target_hex = self.data.recruit.target_hex
|
||||
local efficiency = self.data.recruit.hp_efficiency
|
||||
local distance_to_enemy, enemy_location
|
||||
if target_hex[1] then
|
||||
distance_to_enemy, enemy_location = AH.get_closest_enemy(target_hex)
|
||||
else
|
||||
distance_to_enemy, enemy_location = AH.get_closest_enemy(best_hex)
|
||||
end
|
||||
|
||||
local gold_limit = 9e99
|
||||
if self.data.castle.loose_gold_limit >= self.data.recruit.cheapest_unit_cost then
|
||||
gold_limit = self.data.castle.loose_gold_limit
|
||||
end
|
||||
--print (self.data.castle.loose_gold_limit .. " " .. self.data.recruit.cheapest_unit_cost .. " " .. gold_limit)
|
||||
|
||||
local recruitable_units = {}
|
||||
|
||||
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
-- Count number of units with the same attack type. Used to avoid recruiting too many of the same unit
|
||||
local attack_types = 0
|
||||
local recruit_count = 0
|
||||
for attack_type, count in pairs(unit_attack_type_count[recruit_id]) do
|
||||
attack_types = attack_types + 1
|
||||
recruit_count = recruit_count + attack_type_count[attack_type]
|
||||
end
|
||||
recruit_count = recruit_count / attack_types
|
||||
local recruit_modifier = 1+recruit_count/50
|
||||
|
||||
-- Use time to enemy to encourage recruiting fast units when the opponent is far away (game is beginning or we're winning)
|
||||
-- Base distance on
|
||||
local recruit_unit
|
||||
if target_hex[1] then
|
||||
recruit_unit = wesnoth.create_unit {
|
||||
type = recruit_id,
|
||||
x = target_hex[1],
|
||||
y = target_hex[2],
|
||||
random_traits = false,
|
||||
name = "X",
|
||||
id = recruit_id .. get_next_id(),
|
||||
random_gender = false
|
||||
}
|
||||
else
|
||||
recruit_unit = wesnoth.create_unit {
|
||||
type = recruit_id,
|
||||
x = best_hex[1],
|
||||
y = best_hex[2],
|
||||
random_traits = false,
|
||||
name = "X",
|
||||
id = recruit_id .. get_next_id(),
|
||||
random_gender = false
|
||||
}
|
||||
end
|
||||
local path, cost = wesnoth.find_path(recruit_unit, enemy_location.x, enemy_location.y, {ignore_units = true})
|
||||
local move_score = wesnoth.unit_types[recruit_id].max_moves / (cost*wesnoth.unit_types[recruit_id].cost^0.5)
|
||||
|
||||
-- Estimate effectiveness on offense and defense
|
||||
local offense_score = recruit_effectiveness[recruit_id]/(wesnoth.unit_types[recruit_id].cost^0.3*recruit_modifier^4)
|
||||
local defense_score = efficiency[recruit_id]/recruit_vulnerability[recruit_id]
|
||||
|
||||
local unit_score = {offense = offense_score, defense = defense_score, move = move_score}
|
||||
recruit_scores[recruit_id] = unit_score
|
||||
for key, score in pairs(unit_score) do
|
||||
if score > best_scores[key] then
|
||||
best_scores[key] = score
|
||||
end
|
||||
end
|
||||
|
||||
if can_slow(recruit_unit) then
|
||||
unit_score["slows"] = true
|
||||
end
|
||||
if wesnoth.match_unit(recruit_unit, { ability = "healing" }) then
|
||||
unit_score["heals"] = true
|
||||
end
|
||||
if wesnoth.match_unit(recruit_unit, { 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 offense_weight = 2.5
|
||||
local defense_weight = 1/hp_ratio^0.5
|
||||
local move_weight = math.max((distance_to_enemy/20)^2, 0.25)
|
||||
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
local scores = recruit_scores[recruit_id]
|
||||
local offense_score = (scores["offense"]/best_scores["offense"])^0.5
|
||||
local defense_score = (scores["defense"]/best_scores["defense"])^0.5
|
||||
local move_score = (scores["move"]/best_scores["move"])^0.5
|
||||
|
||||
local score = offense_score*offense_weight + defense_score*defense_weight + move_score*move_weight
|
||||
|
||||
local bonus = 0
|
||||
if scores["slows"] then
|
||||
bonus = bonus + 0.4
|
||||
end
|
||||
if scores["heals"] then
|
||||
bonus = bonus + (healable_count/(healer_count+1))/20
|
||||
end
|
||||
if scores["skirmisher"] then
|
||||
bonus = bonus + 0.1
|
||||
end
|
||||
for attack_range, count in pairs(unit_attack_range_count[recruit_id]) do
|
||||
bonus = bonus + 0.02 * most_common_range_count / (attack_range_count[attack_range]+1)
|
||||
end
|
||||
bonus = bonus + 0.03 * wesnoth.races[wesnoth.unit_types[recruit_id].__cfg.race].num_traits^2
|
||||
if target_hex[1] then
|
||||
recruitable_units[recruit_id].x = best_hex[1]
|
||||
recruitable_units[recruit_id].y = best_hex[2]
|
||||
local path, cost = wesnoth.find_path(recruitable_units[recruit_id], target_hex[1], target_hex[2], {viewing_side=0, max_cost=wesnoth.unit_types[recruit_id].max_moves+1})
|
||||
print( cost .. "?".. wesnoth.unit_types[recruit_id].max_moves)
|
||||
if cost > wesnoth.unit_types[recruit_id].max_moves then
|
||||
-- large penalty if the unit can't reach the target village
|
||||
bonus = bonus - 1
|
||||
end
|
||||
end
|
||||
score = score + bonus
|
||||
|
||||
if AH.print_exec() then
|
||||
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
|
||||
recruit_type = recruit_id
|
||||
end
|
||||
end
|
||||
|
||||
return recruit_type
|
||||
end
|
||||
|
||||
function get_unit_counts_for_healing()
|
||||
local healers = #AH.get_live_units {
|
||||
side = wesnoth.current.side,
|
||||
ability = "healing"
|
||||
}
|
||||
local healable = #AH.get_live_units {
|
||||
side = wesnoth.current.side,
|
||||
{ "not", { ability = "regenerates" }}
|
||||
}
|
||||
return healers, healable
|
||||
end
|
||||
|
||||
function get_village_target(leader, data)
|
||||
-- Only consider villages reachable by our fastest unit
|
||||
local fastest_unit_speed = 0
|
||||
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
if wesnoth.unit_types[recruit_id].max_moves > fastest_unit_speed then
|
||||
fastest_unit_speed = wesnoth.unit_types[recruit_id].max_moves
|
||||
end
|
||||
end
|
||||
|
||||
local locsx, locsy = AH.split_location_list_to_strings(data.castle.locs)
|
||||
|
||||
-- get a list of all unowned villages within fastest_unit_speed
|
||||
-- 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 then
|
||||
exclude_x = table.concat(data.castle.assigned_villages_x, ",")
|
||||
exclude_y = table.concat(data.castle.assigned_villages_y, ",")
|
||||
end
|
||||
local villages = wesnoth.get_locations {
|
||||
terrain = '*^V*',
|
||||
owner_side = 0,
|
||||
{ "and", {
|
||||
radius = fastest_unit_speed,
|
||||
x = locsx, y = locsy
|
||||
}},
|
||||
{ "not", {
|
||||
x = exclude_x,
|
||||
y = exclude_y
|
||||
}}
|
||||
}
|
||||
|
||||
local hex, target, shortest_distance = {}, {}, AH.no_path
|
||||
|
||||
if not data.castle.assigned_villages_x then
|
||||
data.castle.assigned_villages_x = {}
|
||||
data.castle.assigned_villages_y = {}
|
||||
for i,v in ipairs(villages) do
|
||||
local path, cost = wesnoth.find_path(leader, v[1], v[2])
|
||||
if cost <= leader.max_moves then
|
||||
table.insert(data.castle.assigned_villages_x, v[1])
|
||||
table.insert(data.castle.assigned_villages_y, v[2])
|
||||
table.remove(villages, i)
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local village_count = #villages
|
||||
local test_units = get_test_units()
|
||||
local num_recruits = #test_units
|
||||
|
||||
local width,height,border = wesnoth.get_map_size()
|
||||
for i,v in ipairs(villages) do
|
||||
local close_castle_hexes = wesnoth.get_locations {
|
||||
x = locsx, y = locsy,
|
||||
{ "and", {
|
||||
x = v[1], y = v[2],
|
||||
radius = fastest_unit_speed
|
||||
}},
|
||||
{ "not", { { "filter", {} } } }
|
||||
}
|
||||
for u,unit in ipairs(test_units) do
|
||||
test_units[u].x = v[1]
|
||||
test_units[u].y = v[2]
|
||||
end
|
||||
|
||||
local viable_village = false
|
||||
local village_best_hex, village_shortest_distance = {}, AH.no_path
|
||||
for j,c in ipairs(close_castle_hexes) do
|
||||
if c[1] > 0 and c[2] > 0 and c[1] <= width and c[2] <= height then
|
||||
local distance = 0
|
||||
for x,unit in ipairs(test_units) do
|
||||
local path, unit_distance = wesnoth.find_path(unit, c[1], c[2], {viewing_side=0, max_cost=fastest_unit_speed+1})
|
||||
distance = distance + unit_distance
|
||||
|
||||
-- Village is only viable if at least one unit can reach it
|
||||
if unit_distance <= unit.max_moves then
|
||||
viable_village = true
|
||||
end
|
||||
end
|
||||
distance = distance / num_recruits
|
||||
|
||||
if distance < village_shortest_distance then
|
||||
village_best_hex = c
|
||||
village_shortest_distance = distance
|
||||
end
|
||||
end
|
||||
end
|
||||
if village_shortest_distance < shortest_distance then
|
||||
hex = village_best_hex
|
||||
target = v
|
||||
shortest_distance = village_shortest_distance
|
||||
end
|
||||
if not viable_village then
|
||||
-- this village could not be reached by any unit
|
||||
-- eliminate it from consideration
|
||||
table.insert(data.castle.assigned_villages_x, v[1])
|
||||
table.insert(data.castle.assigned_villages_y, v[2])
|
||||
village_count = village_count - 1
|
||||
end
|
||||
end
|
||||
|
||||
table.insert(data.castle.assigned_villages_x, target[1])
|
||||
table.insert(data.castle.assigned_villages_y, target[2])
|
||||
data.castle.loose_gold_limit = math.floor(wesnoth.sides[wesnoth.current.side].gold/village_count + 0.5)
|
||||
|
||||
return hex, target
|
||||
end
|
||||
|
||||
function get_test_units()
|
||||
local test_units, num_recruits = {}, 0
|
||||
local movetypes = {}
|
||||
for x,id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
|
||||
local custom_movement = H.get_child(wesnoth.unit_types[id].__cfg, "movement_costs")
|
||||
local movetype = wesnoth.unit_types[id].__cfg.movement_type
|
||||
if custom_movement
|
||||
or (not movetypes[movetype])
|
||||
or (movetypes[movetype] > wesnoth.unit_types[id].max_moves)
|
||||
then
|
||||
if not custom_movement then
|
||||
movetypes[movetype] = wesnoth.unit_types[id].max_moves
|
||||
end
|
||||
num_recruits = num_recruits + 1
|
||||
test_units[num_recruits] = wesnoth.create_unit({
|
||||
type = id,
|
||||
side = wesnoth.current.side,
|
||||
random_traits = false,
|
||||
name = "X",
|
||||
id = id .. get_next_id(),
|
||||
random_gender = false
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
return test_units
|
||||
end
|
||||
end -- init()
|
||||
}
|
385
data/ai/lua/generic-rush_engine.lua
Normal file
385
data/ai/lua/generic-rush_engine.lua
Normal file
|
@ -0,0 +1,385 @@
|
|||
return {
|
||||
init = function(ai)
|
||||
|
||||
local generic_rush = {}
|
||||
-- More generic grunt rush (and can, in fact, be used with other unit types as well)
|
||||
|
||||
local H = wesnoth.require "lua/helper.lua"
|
||||
local W = H.set_wml_action_metatable {}
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local LS = wesnoth.require "lua/location_set.lua"
|
||||
|
||||
------ 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()
|
||||
print(' Beginning of Turn ' .. wesnoth.current.turn .. ' (' .. tod.name ..') stats (CPU time ' .. os.clock() .. ')')
|
||||
|
||||
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 --------------
|
||||
|
||||
wesnoth.require("ai/lua/generic-recruit_engine.lua").init(ai, generic_rush)
|
||||
|
||||
-------- Castle Switch CA --------------
|
||||
|
||||
function generic_rush:castle_switch_eval()
|
||||
local start_time, ca_name = os.clock(), 'castle_switch'
|
||||
if AH.print_eval() then print(' - Evaluating castle_switch CA:', os.clock()) end
|
||||
|
||||
local leader = wesnoth.get_units {
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = 'yes',
|
||||
formula = '($this_unit.moves = $this_unit.max_moves) and ($this_unit.hitpoints = $this_unit.max_hitpoints)'
|
||||
}[1]
|
||||
if not leader then
|
||||
-- CA is irrelevant if no leader
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return 0
|
||||
end
|
||||
|
||||
local width,height,border = wesnoth.get_map_size()
|
||||
local keeps = wesnoth.get_locations {
|
||||
terrain = "K*^*,*^Kov", -- 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*^*,*^Kov",
|
||||
radius = 2,
|
||||
{ "filter_radius", { terrain = 'C*^*,K*^*,*^Kov,*^Cov' } }
|
||||
}} -- That are not close and connected to a keep the leader is on
|
||||
}
|
||||
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
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return 0
|
||||
end
|
||||
|
||||
local enemy_leaders = AH.get_live_units { canrecruit = 'yes',
|
||||
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
|
||||
}
|
||||
|
||||
-- Look for the best keep
|
||||
local best_score, best_loc = 0, {}
|
||||
for i,loc in ipairs(keeps) do
|
||||
-- Only consider keeps within 3 turns movement
|
||||
local path, cost = wesnoth.find_path(leader, loc[1], loc[2])
|
||||
local score = 0
|
||||
-- Prefer closer keeps to enemy
|
||||
local turns = cost/leader.max_moves
|
||||
if turns <= 2 and turns > 0 then
|
||||
score = 1/(math.ceil(turns))
|
||||
for j,e in ipairs(enemy_leaders) do
|
||||
score = score + 1 / H.distance_between(loc[1], loc[2], e.x, e.y)
|
||||
end
|
||||
|
||||
if score > best_score then
|
||||
best_score = score
|
||||
best_loc = loc
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if best_score > 0 then
|
||||
self.data.target_keep = best_loc
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return 290000
|
||||
end
|
||||
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
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(' ' .. os.clock() .. ' Executing castle_switch CA') end
|
||||
if AH.show_messages() then W.message { speaker = leader.id, message = 'Switching castles' } end
|
||||
|
||||
local x, y = self.data.target_keep[1], self.data.target_keep[2]
|
||||
local next_hop = AH.next_hop(leader, x, y)
|
||||
if next_hop and ((next_hop[1] ~= leader.x) or (next_hop[2] ~= leader.y)) then
|
||||
local path, cost = wesnoth.find_path(leader, x, y)
|
||||
local turn_cost = math.ceil(cost/leader.max_moves)
|
||||
|
||||
-- See if there is a nearby village that can be captured without delaying progress
|
||||
local close_villages = wesnoth.get_locations {
|
||||
{ "and", { x = next_hop[1], y = next_hop[2], radius = 3 }},
|
||||
terrain = "*^V*",
|
||||
owner_side = 0 }
|
||||
local cheapest_unit_cost = AH.get_cheapest_recruit_cost()
|
||||
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, x, y)
|
||||
local turns_from_keep = math.ceil(cost_keep/leader.max_moves)
|
||||
if turns_from_keep < turn_cost
|
||||
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
|
||||
|
||||
ai.move(leader, next_hop[1], next_hop[2])
|
||||
end
|
||||
end
|
||||
|
||||
------- Grab Villages CA --------------
|
||||
|
||||
function generic_rush:grab_villages_eval()
|
||||
local start_time, ca_name = os.clock(), 'grab_villages'
|
||||
if AH.print_eval() then print(' - Evaluating grab_villages CA:', os.clock()) end
|
||||
|
||||
-- Check if there are units with moves left
|
||||
local units = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'no',
|
||||
formula = '$this_unit.moves > 0'
|
||||
}
|
||||
if (not units[1]) then
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return 0
|
||||
end
|
||||
|
||||
local enemies = AH.get_live_units {
|
||||
{ "filter_side", {{"enemy_of", {side = wesnoth.current.side} }} }
|
||||
}
|
||||
|
||||
local villages = wesnoth.get_locations { terrain = '*^V*' }
|
||||
-- Just in case:
|
||||
if (not villages[1]) then
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
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 = AH.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
|
||||
|
||||
-- Now we go on to the unit-dependent rating
|
||||
local best_unit_rating = 0
|
||||
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 unit_in_way and ((unit_in_way.x ~= u.x) or (unit_in_way.y ~= u.y)) 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 = H.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
|
||||
-- Finally, since these can be reached by the enemy, want the strongest unit to go first
|
||||
rating = rating + u.hitpoints / 100.
|
||||
|
||||
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
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return return_value
|
||||
else
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return 0
|
||||
end
|
||||
end
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return 0
|
||||
end
|
||||
|
||||
function generic_rush:grab_villages_exec()
|
||||
if AH.print_exec() then print(' ' .. os.clock() .. ' Executing grab_villages CA') end
|
||||
if AH.show_messages() then W.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 = os.clock(), 'spread_poison'
|
||||
if AH.print_eval() then print(' - Evaluating spread_poison CA:', os.clock()) 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 = '$this_unit.attacks_left > 0',
|
||||
{ "filter_wml", {
|
||||
{ "attack", {
|
||||
{ "specials", {
|
||||
{ "poison", { } }
|
||||
} }
|
||||
} }
|
||||
} },
|
||||
canrecruit = 'no'
|
||||
}
|
||||
--print('#poisoners', #poisoners)
|
||||
if (not poisoners[1]) then
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return 0
|
||||
end
|
||||
|
||||
local attacks = AH.get_attacks(poisoners)
|
||||
--print('#attacks', #attacks)
|
||||
if (not attacks[1]) then
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
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.not_living
|
||||
|
||||
-- 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 is very bad
|
||||
local about_to_level = defender.max_experience - defender.experience <= wesnoth.unit_types[attacker.type].level
|
||||
|
||||
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 / 2.
|
||||
|
||||
-- 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 / 100.
|
||||
--print('rating', rating)
|
||||
|
||||
if rating > max_rating then
|
||||
max_rating, best_attack = rating, a
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if (max_rating > -9e99) then
|
||||
self.data.attack = best_attack
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
return 190000
|
||||
end
|
||||
AH.done_eval_messages(start_time, ca_name)
|
||||
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 AH.print_exec() then print(' ' .. os.clock() .. ' Executing spread_poison CA') end
|
||||
if AH.show_messages() then W.message { speaker = attacker.id, message = 'Poison attack' } end
|
||||
|
||||
local defender = wesnoth.get_unit(self.data.attack.target.x, self.data.attack.target.y)
|
||||
|
||||
AH.movefull_stopunit(ai, attacker, self.data.attack.dst.x, self.data.attack.dst.y)
|
||||
|
||||
-- Find the poison weapon
|
||||
-- If several attacks have poison, this will always find the last one
|
||||
local is_poisoner, poison_weapon = AH.has_weapon_special(attacker, "poison")
|
||||
|
||||
ai.attack(attacker, defender, poison_weapon)
|
||||
|
||||
self.data.attack = nil
|
||||
end
|
||||
|
||||
return generic_rush
|
||||
end
|
||||
}
|
|
@ -3,6 +3,10 @@ changes may be omitted). For a complete list of changes, see the main
|
|||
changelog: http://svn.gna.org/viewcvs/*checkout*/wesnoth/trunk/changelog
|
||||
|
||||
Version 1.11.0+svn:
|
||||
* AI:
|
||||
* New AI: Experimental AI
|
||||
* Improved recruitment, castle and village management over current default AI.
|
||||
|
||||
* Campaigns:
|
||||
* Dead Water:
|
||||
* Stunned units are now marked with a status icon.
|
||||
|
|
Loading…
Add table
Reference in a new issue