Improvements to experimental multiplayer AI recruiting

This commit is contained in:
Simon Forsyth 2012-12-22 03:03:48 +00:00
parent 699a6ae1f3
commit 5d7c5f6fe6
5 changed files with 280 additions and 141 deletions

View file

@ -1,4 +1,9 @@
Version 1.11.1+svn:
* AI:
* Experimental Multiplayer AI
* Improve recruitment, notably first turn choices and units with poison
* Improved selection of units for village stealing
* Remove dependency on AI-demos add-on
* Graphics:
* Fix layering error with bridges
* Language and i18n:

View file

@ -1268,4 +1268,18 @@ function ai_helper.get_attack_combos(units, enemy, cfg)
return attack_array
end
function ai_helper.get_unit_time_of_day_bonus(alignment, lawful_bonus)
local multiplier = 1
if (lawful_bonus ~= 0) then
if (alignment == 'lawful') then
multiplier = (1 + lawful_bonus / 100.)
elseif (alignment == 'chaotic') then
multiplier = (1 - lawful_bonus / 100.)
elseif (alignment == 'liminal') then
multipler = (1 - math.abs(lawful_bonus) / 100.)
end
end
return multiplier
end
return ai_helper

View file

@ -5,27 +5,33 @@ return {
-- 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
-- ai_cas also has the functions find_best_recruit, find_best_recruit_hex and analyze_enemy_unit added to it
-- find_best_recruit, find_best_recruit_hex may be useful for writing recruitment code separately from the engine
-- 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
-- params: parameters to configure recruitment
-- score_function: function that returns the CA score when recruit_rushers_eval wants to recruit
-- (default returns the RCA recruitment score)
-- randomness: a measure of randomness in recruitment
-- higher absolute values increase randomness, with values above about 3 being close to completely random
-- (default = 0.1)
-- 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)
init = function(ai, ai_cas, params)
if not params then
params = {}
end
math.randomseed(os.time())
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)()
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local LS = wesnoth.require "lua/location_set.lua"
local recruit_data = {}
local no_village_cost = function(recruit_id)
return wesnoth.unit_types[recruit_id].cost+wesnoth.unit_types[recruit_id].level+wesnoth.sides[wesnoth.current.side].village_gold
end
local get_hp_efficiency = function (table, recruit_id)
-- raw durability is a function of hp and the regenerates ability
-- efficiency decreases faster than cost increases to avoid recruiting many expensive units
@ -40,7 +46,6 @@ return {
type = recruit_id,
random_traits = false,
name = "X",
id = recruit_id .. get_next_id(),
random_gender = false
}
-- Find the best regeneration ability and use it to estimate hp regained by regeneration
@ -54,10 +59,12 @@ return {
end
effective_hp = effective_hp + (regen_amount * effective_hp/30)
end
local efficiency = math.max(math.log(effective_hp/20),0.01)/(wesnoth.unit_types[recruit_id].cost^2)
local hp_score = math.max(math.log(effective_hp/20),0.01)
local efficiency = hp_score/(wesnoth.unit_types[recruit_id].cost^2)
local no_village_efficiency = hp_score/(no_village_cost(recruit_id)^2)
table[recruit_id] = efficiency
return efficiency
table[recruit_id] = {efficiency, no_village_efficiency}
return {efficiency, no_village_efficiency}
end
local efficiency = {}
setmetatable(efficiency, { __index = get_hp_efficiency })
@ -81,7 +88,7 @@ return {
end
function analyze_enemy_unit(enemy_type, ally_type)
local function get_best_attack(attacker, defender, unit_defense, can_poison)
local function get_best_attack(attacker, defender, defender_defense, attacker_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
@ -93,15 +100,17 @@ return {
-- 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")
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
-- This may be rectifiable by looking at retaliation damage as well.
local steadfast = false
for attack in H.child_range(wesnoth.unit_types[attacker.type].__cfg, "attack") do
local defense = unit_defense
local defense = defender_defense
local poison = false
local damage_multiplier = 1
-- TODO: handle more abilities (charge, drain)
-- TODO: handle more abilities (charge)
for special in H.child_range(attack, 'specials') do
local mod
if H.get_child(special, 'poison') and can_poison then
@ -126,7 +135,7 @@ return {
if mod then
if mod.backstab then
-- Assume backstab happens on only 1/2 of attacks
-- TODO: find out what actual probability is
-- TODO: find out what actual probability of getting to backstab is
damage_multiplier = damage_multiplier*(mod.multiply*0.5 + 0.5)
end
end
@ -138,10 +147,10 @@ return {
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
-- TODO: calculate 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
drain_recovery = (defender_attack.damage*defender_attack.number*attacker_resistance*attacker_defense/2)/10000
end
end
end
@ -170,16 +179,16 @@ return {
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
if (not best_attack) or (attack_damage+poison_damage > best_damage+best_poison_damage) then
best_damage = attack_damage
best_poison_damage = poison_damage
best_attack = attack
end
end
return best_attack, best_damage, poison_damage
return best_attack, best_damage, best_poison_damage
end
-- Use cached information when possible: this is expensive
@ -199,7 +208,6 @@ return {
type = enemy_type,
random_traits = false,
name = "X",
id = enemy_type .. get_next_id(),
random_gender = false
}
local can_poison = living(unit) or wesnoth.unit_ability(unit, 'regenerate')
@ -210,13 +218,15 @@ return {
type = ally_type,
random_traits = false,
name = "X",
id = ally_type .. get_next_id(),
random_gender = false
}
local recruit_flat_defense = wesnoth.unit_defense(recruit, "Gt")
local recruit_best_defense = get_best_defense(recruit)
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)
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)
local result = {
offense = { attack = best_flat_attack, damage = best_flat_damage, poison_damage = flat_poison },
@ -242,6 +252,21 @@ return {
end
function get_hp_ratio_with_gold()
function sum_gold_for_sides(side_filter)
-- sum positive amounts of gold for a set of sides
-- positive only because it is used to estimate the number of enemy units that could appear
-- and negative numbers should't subtract from the number of units on the map
local gold = 0
local sides = wesnoth.get_sides(side_filter)
for i,s in ipairs(sides) do
if s.gold > 0 then
gold = gold + s.gold
end
end
return gold
end
-- Hitpoint ratio of own units / enemy units
-- Also convert available gold to a hp estimate
my_units = AH.get_live_units {
@ -255,13 +280,8 @@ return {
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
my_hp = my_hp + sum_gold_for_sides({{"allied_with", {side = wesnoth.current.side} }})*2.3
enemy_hp = enemy_hp+sum_gold_for_sides({{"enemy_of", {side = wesnoth.current.side} }})*2.3
hp_ratio = my_hp/(enemy_hp + 1e-6)
return hp_ratio
@ -281,6 +301,7 @@ return {
return 0
end
-- Check for space to recruit a unit
get_current_castle(leader, data)
local no_space = true
for i,c in ipairs(data.castle.locs) do
@ -294,11 +315,23 @@ return {
return 0
end
-- Check for minimal recruit option
if wesnoth.current.turn == 1 and params.min_turn_1_recruit and params.min_turn_1_recruit() then
if not get_village_target(leader, data)[1] then
return 0
end
end
if data.recruit == nil then
data.recruit = init_data(leader)
end
data.recruit.cheapest_unit_cost = cheapest_unit_cost
return score_function()
local score = 180000 -- default score if one not provided. Same as RCA AI
if params.score_function then
score = params.score_function()
end
return score
end
function init_data(leader)
@ -310,6 +343,7 @@ return {
}
local enemy_counts = {}
local enemy_types = {}
local possible_enemy_recruit_count = 0
local function add_unit_type(unit_type)
if enemy_counts[unit_type] == nil then
@ -329,6 +363,7 @@ return {
{ "enemy_of", {side = wesnoth.current.side} },
{ "has_unit", { canrecruit = true }} })
for i, side in ipairs(enemy_sides) do
possible_enemy_recruit_count = possible_enemy_recruit_count + #(wesnoth.sides[side.side].recruit)
for j, unit_type in ipairs(wesnoth.sides[side.side].recruit) do
add_unit_type(unit_type)
end
@ -336,6 +371,7 @@ return {
data.enemy_counts = enemy_counts
data.enemy_types = enemy_types
data.num_enemies = #enemies
data.possible_enemy_recruit_count = possible_enemy_recruit_count
return data
end
@ -371,33 +407,43 @@ return {
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
local poisoner_count = 0.1 -- Number of units with a poison attack (set to slightly > 0 because we divide by it later)
local poisonable_count = 0 -- Number of units that the opponents control that are hurt by poison
local recruit_count = {}
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
recruit_count[recruit_id] = #(AH.get_live_units { side = wesnoth.current.side, type = recruit_id, canrecruit = 'no' })
end
for i, unit_type in ipairs(enemy_types) do
enemy_type_count = enemy_type_count + 1
local poison_vulnerable = false
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
local analysis = analyze_enemy_unit(unit_type, recruit_id)
-- 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_effectiveness[recruit_id] = {damage = 0, poison_damage = 0}
recruit_vulnerability[recruit_id] = 0
end
recruit_effectiveness[recruit_id] = recruit_effectiveness[recruit_id] + analysis.defense.damage * enemy_counts[unit_type]^2
recruit_effectiveness[recruit_id].damage = recruit_effectiveness[recruit_id].damage + analysis.defense.damage * enemy_counts[unit_type]^2
if analysis.defense.poison_damage and analysis.defense.poison_damage > 0 then
poison_vulnerable = true
recruit_effectiveness[recruit_id].poison_damage = recruit_effectiveness[recruit_id].poison_damage +
analysis.defense.poison_damage * enemy_counts[unit_type]^2
end
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
attack_type_count[attack_type] = 0
end
attack_type_count[attack_type] = attack_type_count[attack_type] + recruit_count
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
attack_range_count[attack_range] = 0
end
attack_range_count[attack_range] = attack_range_count[attack_range] + recruit_count
attack_range_count[attack_range] = attack_range_count[attack_range] + recruit_count[recruit_id]
if unit_attack_type_count[recruit_id] == nil then
unit_attack_type_count[recruit_id] = {}
@ -409,15 +455,30 @@ return {
end
unit_attack_range_count[recruit_id][attack_range] = true
end
if poison_vulnerable then
poisonable_count = poisonable_count + enemy_counts[unit_type]
end
end
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
-- Count the number of units with the poison ability
-- This could be wrong if all the units on the enemy side are immune to poison, but since poison has no effect then anyway it doesn't matter
if recruit_effectiveness[recruit_id].poison_damage > 0 then
poisoner_count = poisoner_count + recruit_count[recruit_id]
end
end
-- Subtract the number of possible recruits for the enemy from the list of poisonable units
-- This works perfectly unless some of the enemy recruits cannot be poisoned (e.g. not_living)
-- However, there is no problem with this since poison is generally less useful in such situations and subtracting them too discourages such recruiting
local poison_modifier = math.max(0, math.min(((poisonable_count-recruit_data.recruit.possible_enemy_recruit_count) / (poisoner_count*5)), 1))^2
for i, recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
-- Ensure effectiveness and vulnerability are positive.
-- Negative values imply that drain is involved and the amount drained is very high
if recruit_effectiveness[recruit_id] <= 0 then
recruit_effectiveness[recruit_id] = 0.01
if recruit_effectiveness[recruit_id].damage <= 0 then
recruit_effectiveness[recruit_id].damage = 0.01
else
recruit_effectiveness[recruit_id] = (recruit_effectiveness[recruit_id] / (num_enemies)^2)^0.5
recruit_effectiveness[recruit_id].damage = (recruit_effectiveness[recruit_id].damage / (num_enemies)^2)^0.5
end
recruit_effectiveness[recruit_id].poison_damage = (recruit_effectiveness[recruit_id].poison_damage / (num_enemies)^2)^0.5 * poison_modifier
if recruit_vulnerability[recruit_id] <= 0 then
recruit_vulnerability[recruit_id] = 0.01
else
@ -485,7 +546,10 @@ return {
get_current_castle(leader, data)
local best_hex, village = get_village_target(leader, data)
if not village[1] then
if village[1] then
table.insert(data.castle.assigned_villages_x, village[1])
table.insert(data.castle.assigned_villages_y, village[2])
else
-- no available village, look for hex closest to enemy leader
local max_rating = -1
for i,c in ipairs(data.castle.locs) do
@ -544,37 +608,49 @@ return {
end
recruit_count = recruit_count / attack_types
local recruit_modifier = 1+recruit_count/50
local efficiency_index = 1
local unit_cost = wesnoth.unit_types[recruit_id].cost
-- 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
local recruit_unit = wesnoth.create_unit {
type = recruit_id,
x = best_hex[1],
y = best_hex[2],
random_traits = false,
name = "X",
random_gender = false
}
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)
local path, cost = wesnoth.find_path(recruit_unit, target_hex[1], target_hex[2], {viewing_side=0, max_cost=wesnoth.unit_types[recruit_id].max_moves+1})
if cost > wesnoth.unit_types[recruit_id].max_moves then
-- Unit cost is effectively higher if cannot reach the village
efficiency_index = 2
unit_cost = no_village_cost(recruit_id)
end
-- Later calculations are based on where the unit will be after initial move
recruit_unit.x = target_hex[1]
recruit_unit.y = target_hex[2]
end
local path, cost = wesnoth.find_path(recruit_unit, enemy_location.x, enemy_location.y, {ignore_units = true})
local time_to_enemy = cost / wesnoth.unit_types[recruit_id].max_moves
local move_score = 1 / (time_to_enemy * unit_cost^0.5)
local eta = math.ceil(time_to_enemy)
if target_hex[1] then
-- expect a 1 turn delay to reach village
eta = eta + 1
end
-- divide the lawful bonus by eta before running it through the function because the function converts from 0 centered to 1 centered
local lawful_bonus = wesnoth.get_time_of_day(wesnoth.current.turn + eta).lawful_bonus / eta^2
local damage_bonus = AH.get_unit_time_of_day_bonus(recruit_unit.__cfg.alignment, lawful_bonus)
-- 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 offense_score =
(recruit_effectiveness[recruit_id].damage*damage_bonus+recruit_effectiveness[recruit_id].poison_damage)
/(wesnoth.unit_types[recruit_id].cost^0.3*recruit_modifier^4)
local defense_score = efficiency[recruit_id][efficiency_index]/recruit_vulnerability[recruit_id]
local unit_score = {offense = offense_score, defense = defense_score, move = move_score}
recruit_scores[recruit_id] = unit_score
@ -601,13 +677,14 @@ return {
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)
local randomness = params.randomness or 0.1
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 bonus = 0
local bonus = math.random()*randomness
if scores["slows"] then
bonus = bonus + 0.4
end
@ -626,10 +703,11 @@ return {
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})
if cost > wesnoth.unit_types[recruit_id].max_moves then
-- large penalty if the unit can't reach the target village
bonus = bonus - 1
-- penalty if the unit can't reach the target village
bonus = bonus - 0.2
end
end
local score = offense_score*offense_weight + defense_score*defense_weight + move_score*move_weight + bonus
if AH.print_exec() then
@ -672,12 +750,11 @@ 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 then
if data.castle.assigned_villages_x ~= nil 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
local villages = wesnoth.get_locations {
terrain = '*^V*',
local villages = wesnoth.get_villages {
owner_side = 0,
{ "and", {
radius = fastest_unit_speed,
@ -694,13 +771,17 @@ return {
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
if 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])
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
end
@ -708,6 +789,14 @@ return {
local village_count = #villages
local test_units = get_test_units()
local num_recruits = #test_units
local total_village_distance = {}
for j,c in ipairs(data.castle.locs) do
c_index = c[1] + c[2]*1000
total_village_distance[c_index] = 0
for i,v in ipairs(villages) do
total_village_distance[c_index] = total_village_distance[c_index] + H.distance_between(c[1], c[2], v[1], v[2])
end
end
local width,height,border = wesnoth.get_map_size()
for i,v in ipairs(villages) do
@ -740,7 +829,10 @@ return {
end
distance = distance / num_recruits
if distance < village_shortest_distance then
if distance < village_shortest_distance
or (distance == village_shortest_distance and distance < AH.no_path
and total_village_distance[c[1] + c[2]*1000] > total_village_distance[village_best_hex[1]+village_best_hex[2]*1000])
then
village_best_hex = c
village_shortest_distance = distance
end
@ -751,6 +843,7 @@ return {
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
@ -760,8 +853,6 @@ return {
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
@ -775,7 +866,7 @@ return {
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)
or (movetypes[movetype] < wesnoth.unit_types[id].max_moves)
then
if not custom_movement then
movetypes[movetype] = wesnoth.unit_types[id].max_moves
@ -786,7 +877,6 @@ return {
side = wesnoth.current.side,
random_traits = false,
name = "X",
id = id .. get_next_id(),
random_gender = false
})
end

View file

@ -34,7 +34,22 @@ return {
------- Recruit CA --------------
wesnoth.require("ai/lua/generic-recruit_engine.lua").init(ai, generic_rush)
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(ai, generic_rush, params)
-------- Castle Switch CA --------------
@ -58,6 +73,10 @@ return {
return 0
end
if self.data.leader_target then
return 290000
end
local width,height,border = wesnoth.get_map_size()
local keeps = wesnoth.get_locations {
terrain = "K*^*,*^Kov", -- Keeps
@ -71,7 +90,10 @@ return {
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
}}, -- That are not close and connected to a keep the leader is on
{ "filter_adjacent_location", {
terrain = 'C*^*,K*^*,*^Kov,*^Cov'
}} -- That are not one-hex keeps
}
if #keeps < 1 then
-- Skip if there aren't extra keeps to evaluate
@ -85,14 +107,14 @@ return {
}
-- Look for the best keep
local best_score, best_loc = 0, {}
local best_score, best_loc, best_turns = 0, {}, 3
for i,loc in ipairs(keeps) do
-- Only consider keeps within 3 turns movement
-- 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 = cost/leader.max_moves
if turns <= 2 and turns > 0 then
local turns = math.ceil(cost/leader.max_moves)
if turns <= 2 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)
@ -101,12 +123,40 @@ return {
if score > best_score then
best_score = score
best_loc = loc
best_turns = turns
end
end
end
if best_score > 0 then
self.data.target_keep = best_loc
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 = 3 }},
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, 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
AH.done_eval_messages(start_time, ca_name)
return 290000
end
@ -121,38 +171,8 @@ return {
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
ai.move(leader, self.data.leader_target[1], self.data.leader_target[2])
self.data.leader_target = nil
end
------- Grab Villages CA --------------
@ -174,7 +194,7 @@ return {
{ "filter_side", {{"enemy_of", {side = wesnoth.current.side} }} }
}
local villages = wesnoth.get_locations { terrain = '*^V*' }
local villages = wesnoth.get_villages()
-- Just in case:
if (not villages[1]) then
AH.done_eval_messages(start_time, ca_name)
@ -329,8 +349,8 @@ return {
-- 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
-- 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
@ -344,13 +364,17 @@ return {
-- 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.
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 / 100.
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

View file

@ -3,6 +3,12 @@ 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.1+svn:
* AI:
* Experimental Multiplayer AI
* Improve recruitment, notably first turn choices and units with poison.
* Improve selection of units for village stealing.
* Remove dependency on AI-demos add-on.
* Language and i18n:
* Updated translations: Italian, Portuguese.