Merge pull request #4600 from mattsc/expai_ca_merge

Merge Experimental AI candidate actions into the default AI
This commit is contained in:
mattsc 2019-12-15 14:15:55 -08:00 committed by GitHub
commit d650a8336d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 955 additions and 516 deletions

View file

@ -13,15 +13,21 @@
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
{AI_CA_GOTO}
{AI_CA_CASTLE_SWITCH}
{AI_CA_RETREAT_INJURED}
# Todo: use grab_villages CA after improvements to unit/village distribution
#{AI_CA_GRAB_VILLAGES}
{AI_CA_SPREAD_POISON}
{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_HIGH_XP_ATTACK}
{AI_CA_COMBAT}
{AI_CA_PLACE_HEALERS}
{AI_CA_HEALING}
{AI_CA_VILLAGES}
{AI_CA_RETREAT}
{AI_CA_MOVE_TO_TARGETS}
{AI_CA_LEADER_SHARES_KEEP}
{AI_CA_MOVE_TO_ANY_ENEMY}
[/stage]
[/ai]

View file

@ -0,0 +1,27 @@
#textdomain wesnoth-ai
#ifndef AI_CA_GOTO
{core/macros/ai_candidate_actions.cfg}
#endif
[ai]
id=ai_default_rca_1_14
description=_"Multiplayer_AI^1.14 Default AI" # wmllint: no spellcheck
mp_rank=1005
# RCA := Register Candidate Action; more info at https://forums.wesnoth.org/viewtopic.php?p=419625#p419625
[stage]
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
{AI_CA_GOTO}
{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_HIGH_XP_ATTACK}
{AI_CA_COMBAT}
{AI_CA_HEALING}
{AI_CA_VILLAGES}
{AI_CA_RETREAT}
{AI_CA_MOVE_TO_TARGETS}
{AI_CA_LEADER_SHARES_KEEP}
[/stage]
[/ai]

View file

@ -1,75 +0,0 @@
#textdomain wesnoth-ai
#ifndef AI_CA_GOTO
{core/macros/ai_candidate_actions.cfg}
#endif
[ai]
id=experimental_ai
description=_"Multiplayer_AI^Experimental AI" # wmllint: no spellcheck
mp_rank=1010
[stage]
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
{AI_CA_GOTO}
#{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_HIGH_XP_ATTACK}
{AI_CA_COMBAT}
{AI_CA_HEALING}
{AI_CA_VILLAGES}
{AI_CA_RETREAT}
{AI_CA_MOVE_TO_TARGETS}
{AI_CA_LEADER_SHARES_KEEP}
[candidate_action]
engine=lua
name=recruit_rushers
max_score=196000
location="ai/lua/ca_recruit_rushers.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=switch_castle
max_score=195000
location="ai/lua/ca_castle_switch.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=retreat_injured
max_score=192000
location="ai/lua/ca_retreat_injured.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=grab_villages
max_score=191000
location="ai/lua/ca_grab_villages.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=spread_poison
max_score=190000
location="ai/lua/ca_spread_poison.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=place_healers
max_score=96000
location="ai/lua/ca_place_healers.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=village_hunt
max_score=30000
location="ai/lua/ca_village_hunt.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=move_to_any_enemy
max_score=1000
location="ai/lua/ca_move_to_any_enemy.lua"
[/candidate_action]
[/stage]
[/ai]

View file

@ -0,0 +1,35 @@
#textdomain wesnoth-ai
#ifndef AI_CA_GOTO
{core/macros/ai_candidate_actions.cfg}
#endif
# Note: The Experimental AI and the default AI are currently almost identical.
# This is a placeholder for future development.
[ai]
id=experimental_ai
description=_"Multiplayer_AI^Experimental AI" # wmllint: no spellcheck
mp_rank=1010
[stage]
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
{AI_CA_GOTO}
{AI_CA_RECRUIT_RUSHERS}
{AI_CA_CASTLE_SWITCH}
{AI_CA_RETREAT_INJURED}
{AI_CA_GRAB_VILLAGES}
{AI_CA_SPREAD_POISON}
#{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_HIGH_XP_ATTACK}
{AI_CA_COMBAT}
{AI_CA_PLACE_HEALERS}
{AI_CA_HEALING}
#{AI_CA_VILLAGES}
{AI_CA_MOVE_TO_TARGETS}
{AI_CA_LEADER_SHARES_KEEP}
{AI_CA_MOVE_TO_ANY_ENEMY}
[/stage]
[/ai]

View file

@ -89,11 +89,17 @@ function ai_helper.put_labels(map, cfg)
-- - keys: (array) if the value to be displayed is a subelement of the LS data,
-- use these keys to access it. For example, if we want to display data[3]
-- set keys = { 3 }, if it's data.arg[3], set keys = { 'arg', 3 }
-- - clear=true: (boolean) if set to 'false', do not clear existing labels
-- - color=nil: (string) the color string to be used for the output
cfg = cfg or {}
local factor = cfg.factor or 1
ai_helper.clear_labels()
local clear_labels = cfg.clear
if (clear_labels == nil) then clear_labels = true end
if clear_labels then
ai_helper.clear_labels()
end
map:iter(function(x, y, data)
local out
@ -111,7 +117,7 @@ function ai_helper.put_labels(map, cfg)
end
if (type(out) == 'number') then out = out * factor end
wesnoth.label { x = x, y = y, text = out }
wesnoth.label { x = x, y = y, text = out, color = cfg.color }
end)
end
@ -1149,6 +1155,7 @@ function ai_helper.get_attackable_enemies(filter, side, cfg)
-- - enemies of the side defined in @side,
-- - not petrified
-- - and visible to the side defined in @cfg.viewing_side.
-- - have at least one adjacent hex that is not inside an area to avoid
-- For speed reasons, this is done separately, rather than calling ai_helper.get_visible_units().
--
-- Optional parameters:
@ -1158,6 +1165,8 @@ function ai_helper.get_attackable_enemies(filter, side, cfg)
-- @side: side number, if side other than current side is to be considered
-- @cfg: table with optional configuration parameters:
-- viewing_side: see comments at beginning of this file. Defaults to @side.
-- avoid_map: if given, an enemy is included only if it does not have at least one
-- adjacent hex outside of avoid_map
side = side or wesnoth.current.side
local viewing_side = cfg and cfg.viewing_side or side
@ -1175,7 +1184,19 @@ function ai_helper.get_attackable_enemies(filter, side, cfg)
and (not unit.status.petrified)
and unit:matches(filter_plus_vision)
then
table.insert(enemies, unit)
local is_avoided = false
if cfg and cfg.avoid_map then
is_avoided = true
for xa,ya in H.adjacent_tiles(unit.x, unit.y) do
if (not cfg.avoid_map:get(xa, ya)) then
is_avoided = false
break
end
end
end
if (not is_avoided) then
table.insert(enemies, unit)
end
end
end
@ -1262,9 +1283,28 @@ function ai_helper.has_weapon_special(unit, special)
return false
end
function ai_helper.get_cheapest_recruit_cost()
local cheapest_unit_cost = math.huge
function ai_helper.get_cheapest_recruit_cost(leader)
-- Optional input @leader: if given, find the cheapest recruit cost for this leader,
-- otherwise for the combination of all leaders of the current side
local recruit_ids = {}
for _,recruit_id in ipairs(wesnoth.sides[wesnoth.current.side].recruit) do
table.insert(recruit_ids, recruit_id)
end
local leaders
if leader then
leaders = { leader }
else
leaders = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }
end
for _,l in ipairs(leaders) do
for _,recruit_id in ipairs(l.extra_recruit) do
table.insert(recruit_ids, recruit_id)
end
end
local cheapest_unit_cost = math.huge
for _,recruit_id in ipairs(recruit_ids) do
if wesnoth.unit_types[recruit_id].cost < cheapest_unit_cost then
cheapest_unit_cost = wesnoth.unit_types[recruit_id].cost
end
@ -1388,39 +1428,107 @@ function ai_helper.next_hop(unit, x, y, cfg)
-- viewing_side: see comments at beginning of this file. Defaults to side of @unit
-- plus:
-- ignore_own_units: if set to true, then own units that can move out of the way are ignored
-- path: if given, find the next hop along this path, rather than doing new path finding
-- In this case, it is assumed that the path is possible, in other words, that cost has been checked
-- avoid_map: a location set with the hexes the unit is not allowed to step on
-- fan_out=true: prior to Wesnoth 1.16, the unit strictly followed the path, which can lead to
-- a line-up of units if there are allied units in the way (e.g. when multiple units are
-- moved by the same candidate action. Now they fan out instead, trying to get as close to
-- the ideal next_hop goal (defined as where the unit could get if there were no allied units
-- in the way) as possible. Setting 'fan_out=false' restores the old behavior. The main
-- disadvantage of the new method is that it needs to do more path finding and therefore takes longer.
local path, cost = ai_helper.find_path_with_shroud(unit, x, y, cfg)
if cost >= ai_helper.no_path then return nil, cost end
local path, cost
if cfg and cfg.path then
path = cfg.path
else
path, cost = ai_helper.find_path_with_shroud(unit, x, y, cfg)
if cost >= ai_helper.no_path then return nil, cost end
end
-- If none of the hexes are unoccupied, use current position as default
local next_hop, nh_cost = { unit.x, unit.y }, 0
local next_hop_ideal = { unit.x, unit.y }
-- Go through loop to find reachable, unoccupied hex along the path
-- Start at second index, as first is just the unit position itself
for i = 2,#path do
local sub_path, sub_cost = ai_helper.find_path_with_shroud(unit, path[i][1], path[i][2], cfg)
if (not cfg) or (not cfg.avoid_map) or (not cfg.avoid_map:get(path[i][1], path[i][2])) then
local sub_path, sub_cost = ai_helper.find_path_with_shroud(unit, path[i][1], path[i][2], cfg)
if sub_cost <= unit.moves then
-- Check for unit in way only if cfg.ignore_units is not set
local unit_in_way
if (not cfg) or (not cfg.ignore_units) then
unit_in_way = wesnoth.units.get(path[i][1], path[i][2])
if sub_cost <= unit.moves then
-- Check for unit in way only if cfg.ignore_units is not set
local unit_in_way
if (not cfg) or (not cfg.ignore_units) then
unit_in_way = wesnoth.units.get(path[i][1], path[i][2])
-- If ignore_own_units is set, ignore own side units that can move out of the way
if cfg and cfg.ignore_own_units then
if unit_in_way and (unit_in_way.side == unit.side) then
local reach = ai_helper.get_reachable_unocc(unit_in_way, cfg)
if (reach:size() > 1) then unit_in_way = nil end
-- If ignore_own_units is set, ignore own side units that can move out of the way
if cfg and cfg.ignore_own_units then
if unit_in_way and (unit_in_way.side == unit.side) then
local reach = ai_helper.get_reachable_unocc(unit_in_way, cfg)
if (reach:size() > 1) then unit_in_way = nil end
end
end
end
end
if not unit_in_way then
next_hop, nh_cost = path[i], sub_cost
if not unit_in_way then
next_hop, nh_cost = path[i], sub_cost
end
next_hop_ideal = path[i]
else
break
end
end
end
local fan_out = cfg and cfg.fan_out
if (fan_out == nil) then fan_out = true end
if fan_out and ((next_hop[1] ~= next_hop_ideal[1]) or (next_hop[2] ~= next_hop_ideal[2]))
then
-- If we cannot get to the ideal next hop, try fanning out instead
local reach = wesnoth.find_reach(unit, cfg)
-- Need the reach map of the unit from the ideal next hop hex
-- There will always be another unit there, otherwise we would not have gotten here
local unit_in_way = wesnoth.get_unit(next_hop_ideal[1], next_hop_ideal[2])
unit_in_way:extract()
local old_x, old_y = unit.x, unit.y
unit:extract()
unit:to_map(next_hop_ideal[1], next_hop_ideal[2])
local inverse_reach = wesnoth.find_reach(unit, { ignore_units = true }) -- no ZoC
unit:extract()
unit:to_map(old_x, old_y)
unit_in_way:to_map()
local terrain = wesnoth.get_terrain(next_hop_ideal[1], next_hop_ideal[2])
local move_cost_endpoint = wesnoth.unit_movement_cost(unit, terrain)
local inverse_reach_map = LS.create()
for _,r in pairs(inverse_reach) do
-- We want the moves left for moving into the opposite direction in which the reach map was calculated
local terrain = wesnoth.get_terrain(r[1], r[2])
local move_cost = wesnoth.unit_movement_cost(unit, terrain)
local inverse_cost = r[3] + move_cost - move_cost_endpoint
inverse_reach_map:insert(r[1], r[2], inverse_cost)
end
local units = ai_helper.get_visible_units(
cfg and cfg.viewing_side or unit.side,
{ { "not", { id = unit.id } }
})
local unit_map = LS.create()
for _,u in ipairs(units) do unit_map:insert(u.x, u.y, u.id) end
local max_rating = inverse_reach_map:get(next_hop[1], next_hop[2]) -- do not move farther away
for _,loc in ipairs(reach) do
if (not unit_map:get(loc[1], loc[2]))
and ((not cfg) or (not cfg.avoid_map) or (not cfg.avoid_map:get(loc[1], loc[2])))
then
local rating = inverse_reach_map:get(loc[1], loc[2]) or - math.huge
if (rating > max_rating) then
max_rating = rating
next_hop = { loc[1], loc[2] } -- eliminating the third argument
end
end
else
break
end
end
@ -1584,6 +1692,187 @@ function ai_helper.find_path_with_shroud(unit, x, y, cfg)
return path, cost
end
function ai_helper.custom_cost_with_avoid(x, y, prev_cost, unit, avoid_map, ally_map, enemy_map, enemy_zoc_map, strict_avoid)
-- Custom cost function for path finding which takes hexes to be avoided into account.
-- See the notes in function ai_helper.find_path_with_avoid()
--
-- For efficiency reasons, this function requires quite a few arguments to be passed to it.
-- Function ai_helper.find_path_with_avoid() does most of this automatically, but the custom cost
-- function can be accessed directly also for even more customized behavior.
if enemy_map and enemy_map:get(x, y) then
return ai_helper.no_path
end
if strict_avoid and avoid_map and avoid_map:get(x, y) then
return ai_helper.no_path
end
local max_moves = unit.max_moves
local terrain = wesnoth.get_terrain(x, y)
local move_cost = wesnoth.unit_movement_cost(unit, terrain)
if (move_cost > max_moves) then
return ai_helper.no_path
end
local prev_moves = math.floor(prev_cost) -- remove all the minor ratings
-- Note that prev_moves_left == max_moves if the unit ended turn on previous hex, as it should
local prev_moves_left = max_moves - (unit.max_moves - unit.moves + prev_moves) % max_moves
if enemy_zoc_map and enemy_zoc_map:get(x,y) then
if (move_cost < prev_moves_left) then
move_cost = prev_moves_left
end
end
local moves_left = prev_moves_left - move_cost
-- Determine whether previous hex was marked as unusable for ending the turn on (in the ones' place
-- after multiplying by 100000), and also how many allied units are lines up along the path (tenth' place)
-- Working with large integers for this part, in order to prevent rounding errors
local prev_cost_int = math.floor(prev_cost * 100000 + 0.001)
local unit_penalty = math.floor((prev_cost * 100000 - prev_cost_int + 0.001) * 10) / 10
local avoid_penalty = math.floor(prev_cost_int - math.floor(prev_cost_int / 10) * 10 + 0.001)
local move_cost_int = math.floor(move_cost * 100000 + 0.001)
-- Apply unit_penalty only for the first turn
local is_first_turn = false
if (prev_moves < unit.moves) then is_first_turn = true end
if is_first_turn then
-- If the hex is both not-avoided and does not have a unit on it, we clear unit_penalty.
-- Otherwise we add in the move cost of the current hex.
-- The purpose of this is to have units spread out rather than move in a line, but note
-- that this only works between paths to the hex that use up the same movement cost.
-- It is fundamentally impossible with the Wesnoth A* search algorithm to make the unit
-- choose a longer path in this way.
if (ally_map and ally_map:get(x, y)) or (avoid_map and avoid_map:get(x, y)) then
unit_penalty = unit_penalty + move_cost / 10
-- We restrict this two 9 MP, even for units with more moves
if (unit_penalty > 0.9) then unit_penalty = 0.9 end
move_cost_int = move_cost_int + unit_penalty
else
move_cost_int = move_cost_int - unit_penalty
unit_penalty = 0
end
end
if (moves_left < 0) then
-- This is the situation when there were moves left on the previous hex,
-- but not enough to enter this hex. In this case, we need to apply the appropriate penalty:
-- - If avoided hex: disqualify it
-- - Otherwise use up full move on previous hex
-- - Also, apply the unit line-up penalty, but only if this is the first move
if (avoid_penalty > 0) then -- avoided hex
return ai_helper.no_path
end
move_cost_int = move_cost_int + prev_moves_left * 100000
if is_first_turn then
move_cost_int = move_cost_int + unit_penalty * 10 * 100000 -- unit_penalty is multiples of 0.1
end
elseif (moves_left == 0) then
-- And this is the case when moving to this hex uses up all moves for the turn
if avoid_map and avoid_map:get(x, y) then
return ai_helper.no_path
end
if is_first_turn then
move_cost_int = move_cost_int + unit_penalty * 10 * 100000 -- unit_penalty is multiples of 0.1
end
end
-- Here's the part that marks the hex as (un)usable
-- We first need to subtract out the previous penalty
move_cost_int = move_cost_int - avoid_penalty
-- Then we need to add in a small number (remember everything is divided by 100000 at the end)
-- because the move cost returned by this functions needs to be >= 1. Use defense for this,
-- thus giving a small bonus (low resulting move cost) for good terrain.
-- Note that the returned cost is rounded to an integer by the engine, so for very long paths this
-- will potentially add to the cost and might make the path inaccurate. However, for an average
-- defense of 50 along the path, this will not happen until the path is 1000 hexes long. Also,
-- in most cases this will simply add to the cost, rather than change the path itself.
local defense = unit:defense(terrain)
-- We need this to be multiples of 10 for the penalty identification to work
defense = H.round(defense / 10) * 10
if (defense > 90) then defense = 90 end
if (defense < 10) then defense = 10 end
move_cost_int = move_cost_int + defense
-- And finally we add a (very small) penalty for this hex if it is to be avoided
-- This is used for the next hex to determine whether the previous hex was to be
-- avoided via avoid_penalty above.
if avoid_map and avoid_map:get(x, y) then
move_cost_int = move_cost_int + 1
end
return move_cost_int / 100000
end
function ai_helper.find_path_with_avoid(unit, x, y, avoid_map, options)
-- Find path while taking hexes to be avoided into account. In its default setting,
-- it also finds the path so that the unit does not end a move on a hex with an allied
-- unit, which is one of the main shortcomings of the default path finder.
--
-- Important notes:
-- - There are two modes of avoiding hexes: the default for which the unit may move through
-- the avoided area but not end a move on it; and a "strict avoid" mode for which the
-- path may not lead through the avoided area at all.
-- - Not ending turns on hexes with allied units is meant to with units moving around each other,
-- but this can cause problems in narrow passages. It can therefore also be turned off.
-- - This cost function does not provide all the configurability of the default path finder.
-- The functionality is as follows:
-- - Hexes with visible enemy units are always excluded, and enemy ZoC is taken into account
-- - Invisible enemies are always ignored (including those under shroud)
-- - Hexes with higher terrain defense are preferred, all else being equal.
--
-- OPTIONAL INPUTS:
-- @options: Note that this is not the same as the @cfg table that can be passed to wesnoth.find_path().
-- Possible fields are:
-- @strict_avoid: if 'true', trigger the "strict avoid" mode described above
-- @ignore_enemies: if 'true', enemies will not be taken into account.
-- @ignore_allies: if 'true', allied units will not be taken into account.
options = options or {}
-- This needs to be done separately, otherwise a path that only goes a short time into the
-- avoided area might not be disqualified correctly. It also saves evaluation time in other cases.
if avoid_map:get(x,y) then
return nil, ai_helper.no_path
end
local all_units = wesnoth.get_units()
local ally_map, enemy_map = LS.create(), LS.create()
for _,u in ipairs(all_units) do
if (u.id ~= unit.id) and ai_helper.is_visible_unit(wesnoth.current.side, u) then
if wesnoth.sides.is_enemy(u.side, wesnoth.current.side) then
if (not options.ignore_enemies) then
enemy_map:insert(u.x, u.y, u.level)
end
else
if (not options.ignore_allies) then
ally_map:insert(u.x, u.y, u.level)
end
end
end
end
local enemy_zoc_map = LS.create()
if (not options.ignore_enemies) and (not unit:ability("skirmisher")) then
enemy_map:iter(function(x, y, level)
if (level > 0) then
for xa,ya in H.adjacent_tiles(x, y) do
enemy_zoc_map:insert(xa, ya, level)
end
end
end)
end
-- Note: even though the cost function returns a float, the engine rounds the cost to an integer
return ai_helper.find_path_with_shroud(unit, x, y, {
calculate = function(xc, yc, current_cost)
return ai_helper.custom_cost_with_avoid(xc, yc, current_cost, unit, avoid_map, ally_map, enemy_map, enemy_zoc_map, options.strict_avoid)
end
})
end
function ai_helper.find_best_move(units, rating_function, cfg)
-- Find the best move and best unit based on @rating_function
-- INPUTS:

View file

@ -1251,7 +1251,7 @@ function battle_calcs.relative_damage_map(units, enemies, cache)
best_enemy = enemy
end
end
unit_ratings[i] = { rating = max_rating, unit_id = u.id, enemy_id = best_enemy.id }
unit_ratings[i] = { rating = max_rating, unit_id = unit.id, enemy_id = best_enemy.id }
end
-- Then we want the same thing for all of the enemy units (for the counter attack on enemy turn)

View file

@ -4,9 +4,12 @@ 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'
-- Note that CS_leader and CS_leader_target are also needed by the recruiting CA, so they must be stored in 'data'
local function get_reachable_enemy_leaders(unit)
local high_score = 195000
local low_score = 15000
local function get_reachable_enemy_leaders(unit, avoid_map)
-- 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.
@ -14,19 +17,51 @@ local function get_reachable_enemy_leaders(unit)
{ "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)
for _,e in ipairs(potential_enemy_leaders) do
-- Cannot use AH.find_path_with_avoid() here as there might be enemies all around the enemy leader
if (not avoid_map:get(e.x, e.y)) then
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
end
return enemy_leaders
end
local function other_units_on_keep(leader)
-- if we're on a keep, wait until there are no movable non-leader units on the castle before moving off
local leader_score = high_score
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.units.get(loc[1], loc[2])
if unit and (unit.side == wesnoth.current.side) and (not unit.canrecruit) and (unit.moves > 0) then
should_wait = true
break
end
end
if should_wait then
leader_score = low_score
end
end
return leader_score
end
local ca_castle_switch = {}
function ca_castle_switch:evaluation(cfg, data, filter_own)
function ca_castle_switch:evaluation(cfg, data, filter_own, recruiting_leader)
-- @recruiting_leader is passed from the recuit_rushers CA for the leader_takes_village()
-- evaluation. If it is set, we do the castle switch evaluation only for that leader
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
@ -35,153 +70,163 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
return 0
end
local leader = wesnoth.units.find_on_map {
local leaders
if recruiting_leader then
-- Note that doing this might set the stored castle switch information to a different leader.
-- This is fine though, the order in which these are done is not particularly important.
leaders = { recruiting_leader }
else
leaders = AH.get_units_with_moves({
side = wesnoth.current.side,
canrecruit = 'yes',
formula = '(movement_left = total_movement) and (hitpoints = max_hitpoints)',
{ "and", filter_own }
}[1]
if not leader then
}, true)
end
if (not leaders[1]) then
-- CA is irrelevant if no leader or the leader may have moved from another CA
data.leader_target = nil
data.CS_leader, data.CS_leader_target = nil, 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()
local avoid_map = AH.get_avoid_map(ai, nil, true)
if data.CS_leader and wesnoth.sides[wesnoth.current.side].gold >= AH.get_cheapest_recruit_cost(data.CS_leader)
and ((not recruiting_leader) or (recruiting_leader.id == data.CS_leader.id))
then
-- If the saved score is the low score, check whether there are still other units on the keep
if (CS_leader_score == low_score) then
CS_leader_score = other_units_on_keep(data.CS_leader)
end
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
local path, cost = AH.find_path_with_avoid(data.CS_leader, data.CS_leader_target[1], data.CS_leader_target[2], avoid_map)
local next_hop = AH.next_hop(data.CS_leader, nil, nil, { path = path, avoid_map = avoid_map })
if next_hop and next_hop[1] == data.CS_leader_target[1]
and next_hop[2] == data.CS_leader_target[2]
then
return CS_leader_score
else
data.CS_leader, data.CS_leader_target = nil, nil
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
local overall_best_score = 0
for _,leader in ipairs(leaders) do
local best_score, best_loc, best_turns, best_path = 0, {}, 3
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
if score > best_score then
best_score = score
best_loc = loc
best_turns = turns
local enemy_leaders = get_reachable_enemy_leaders(leader, avoid_map)
for i,loc in ipairs(keeps) do
-- Only consider keeps within 2 turns movement
local path, cost = AH.find_path_with_avoid(leader, loc[1], loc[2], avoid_map)
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
best_path = path
end
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 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
if score > best_score then
best_score = 0
end
end
end
end
if best_score > 0 then
local next_hop = AH.next_hop(leader, best_loc[1], best_loc[2])
if best_score > 0 then
local next_hop = AH.next_hop(leader, nil, nil, { path = best_path, avoid_map = avoid_map })
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
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 })
local cheapest_unit_cost = AH.get_cheapest_recruit_cost(leader)
for i,loc in ipairs(close_villages) do
local path_village, cost_village = AH.find_path_with_avoid(leader, loc[1], loc[2], avoid_map)
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], avoid_map)
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
end
data.leader_target = next_hop
local leader_score = other_units_on_keep(leader)
best_score = best_score + leader_score
-- 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.units.get(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
if (best_score > overall_best_score) then
overall_best_score = best_score
CS_leader_score = leader_score
data.CS_leader = leader
data.CS_leader_target = next_hop
end
end
end
if (overall_best_score > 0) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return CS_leader_score
end
@ -191,17 +236,11 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
end
function ca_castle_switch:execution(cfg, data, filter_own)
local leader = wesnoth.units.find_on_map {
side = wesnoth.current.side,
canrecruit = 'yes',
{ "and", filter_own }
}[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
AH.robust_move_and_attack(ai, data.CS_leader, data.CS_leader_target, nil, { partial_move = true })
data.CS_leader, data.CS_leader_target = nil
end
return ca_castle_switch

View file

@ -2,6 +2,7 @@
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 GV_unit, GV_village
@ -25,8 +26,15 @@ function ca_grab_villages:evaluation(cfg, data, filter_own)
local enemies = AH.get_attackable_enemies()
local villages = wesnoth.get_villages()
-- Just in case:
local avoid_map = LS.of_pairs(ai.aspects.avoid)
local all_villages, villages = wesnoth.get_villages(), {}
for _,village in ipairs(all_villages) do
if (not avoid_map:get(village[1], village[2])) then
table.insert(villages, village)
end
end
if (not villages[1]) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0

View file

@ -3,6 +3,7 @@
-- 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 H = wesnoth.require "helper"
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local MTAE_unit, MTAE_destination
@ -13,11 +14,11 @@ function ca_move_to_any_enemy:evaluation(cfg, data, filter_own)
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 {
local units = AH.get_units_with_moves({
side = wesnoth.current.side,
canrecruit = 'no',
{ "and", filter_own }
}
}, true)
if (not units[1]) then
-- No units with moves left
@ -25,41 +26,52 @@ function ca_move_to_any_enemy:evaluation(cfg, data, filter_own)
return 0
end
local avoid_map = AH.get_avoid_map(ai, nil, true)
-- In principle we don't even need to pass avoid_map here, as the loop below also
-- checks this, but we might as well eliminate unreachable enemies right away
local enemies = AH.get_attackable_enemies({}, wesnoth.current.sude, { avoid_map = avoid_map })
local unit, destination
-- Find a unit that has a path to an space close to an enemy
-- Find first unit that can reach a hex adjacent to an enemy, and find closest enemy of those reachable.
-- This does not need to find the absolutely best combination, close to that is good enough.
for i,u in ipairs(units) do
local 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[1] == unit.x) and (destination[2] == unit.y) then
destination = nil
local best_cost, best_path, best_enemy = AH.no_path
for i,e in ipairs(enemies) do
-- We only need to look at adjacent hexes. And we don't worry whether they
-- are occupied by other enemies. If that is the case, no path will be found,
-- but one of those enemies will later be found as potential target.
for xa,ya in H.adjacent_tiles(e.x, e.y) do
if (not avoid_map:get(xa, ya)) then
local path, cost = AH.find_path_with_avoid(u, xa, ya, avoid_map)
if (cost < best_cost) then
best_cost = cost
best_path = path
best_enemy = e
-- We also don't care if this is the closest adjacent hex, just pick the first found
break
end
end
end
end
if destination then
break
if best_enemy then
MTAE_destination = AH.next_hop(u, nil, nil, { path = best_path, avoid_map = avoid_map })
if (MTAE_destination[1] ~= u.x) or (MTAE_destination[2] ~= u.y) then
MTAE_unit = u
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 1000
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
return 0
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])
AH.robust_move_and_attack(ai, MTAE_unit, MTAE_destination)
MTAE_unit, MTAE_destination = nil,nil
end

View file

@ -17,11 +17,11 @@ 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
params.leader_takes_village = (function(leader)
if ca_castle_switch:evaluation({}, dummy_engine.data, nil, leader) > 0 then
local take_village = #(wesnoth.get_villages {
x = dummy_engine.data.leader_target[1],
y = dummy_engine.data.leader_target[2]
x = dummy_engine.data.CS_leader_target[1],
y = dummy_engine.data.CS_leader_target[2]
}) > 0
return take_village
end

View file

@ -1,6 +1,7 @@
------- Retreat CA --------------
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local LS = wesnoth.require "location_set"
local R = wesnoth.require "ai/lua/retreat.lua"
local retreat_unit, retreat_loc
@ -11,11 +12,17 @@ function ca_retreat_injured:evaluation(cfg, data, filter_own)
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
if (ai.aspects.caution <= 0) 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,
{ "and", filter_own }
}, true)
local unit, loc = R.retreat_injured_units(units)
local avoid_map = LS.of_pairs(ai.aspects.avoid)
local unit, loc = R.retreat_injured_units(units, avoid_map)
if unit then
retreat_unit = unit
retreat_loc = loc

View file

@ -1,6 +1,7 @@
------- Spread Poison CA --------------
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 SP_attack
@ -11,36 +12,41 @@ function ca_spread_poison:evaluation(cfg, data, filter_own)
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',
{ "and", filter_own }
}
local attacks_aspect = ai.aspects.attacks
local poisoners = {}
for _,unit in ipairs(attacks_aspect.own) do
if (unit.attacks_left > 0) and (#unit.attacks > 0) and AH.has_weapon_special(unit, "poison")
and (not unit.canrecruit) and unit:matches(filter_own)
then
table.insert(poisoners, unit)
end
end
if (not poisoners[1]) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
local target_map = LS.create()
for _,enemy in ipairs(attacks_aspect.enemy) do
target_map:insert(enemy.x, enemy.y)
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
local aggression = ai.aspects.aggression
if (aggression > 1) then aggression = 1 end
local avoid_map = LS.of_pairs(ai.aspects.avoid)
-- Go through all possible attacks with poisoners
local max_rating, best_attack = - math.huge
for i,a in ipairs(attacks) do
if (not avoid_map:get(a.dst.x, a.dst.y)) then
if target_map:get(a.target.x, a.target.y) and (not avoid_map:get(a.dst.x, a.dst.y)) then
local attacker = wesnoth.units.get(a.src.x, a.src.y)
local defender = wesnoth.units.get(a.target.x, a.target.y)
@ -55,30 +61,30 @@ function ca_spread_poison:evaluation(cfg, data, filter_own)
local about_to_level = defender.max_experience - defender.experience <= (attacker.level * 2 * wesnoth.game_config.combat_experience)
if (not cant_poison) and (healing == 0) and (not about_to_level) then
-- Strongest enemy gets poisoned first
local rating = defender.hitpoints
local _, poison_weapon = AH.has_weapon_special(attacker, "poison")
local dst = { a.dst.x, a.dst.y }
local att_stats, def_stats = BC.simulate_combat_loc(attacker, dst, defender, poison_weapon)
local _, defender_rating, attacker_rating = BC.attack_rating(attacker, defender, dst, { att_stats = att_stats, def_stats = def_stats })
-- 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
-- As this is the spread poison CA, we want to emphasize poison damage more, but only for non-regenerating units.
-- For regenerating units this is actually a penalty, as the poison might be more useful elsewhere.
local additional_poison_rating = wesnoth.game_config.poison_amount * (def_stats.poisoned - def_stats.hp_chance[0])
additional_poison_rating = additional_poison_rating / defender.max_hitpoints * defender.cost
if defender:ability('regenerate') then
additional_poison_rating = - additional_poison_rating
end
-- More priority to enemies on strong terrain
local defender_defense = 100 - defender:defense(defender_terrain)
rating = rating + defender_defense / 4.
local defense_rating = (100 - defender:defense(defender_terrain)) / 100
-- 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.
attacker_rating = attacker_rating * (1 - aggression)
local combat_rating = attacker_rating + defender_rating + additional_poison_rating
local total_rating = combat_rating + defense_rating
-- 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
-- Only do the attack if combat_rating is positive. As there is a sizable
-- bonus for poisoning, this will be the case for most attacks.
if (combat_rating > 0) and (total_rating > max_rating) then
max_rating, best_attack = total_rating, a
end
end
end

View file

@ -1,8 +1,12 @@
------- 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
-- 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,
-- but only in the area not prohibited by an [avoid] directive.
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local LS = wesnoth.require "location_set"
local VH_unit, VH_dst = {}, {}
local ca_village_hunt = {}
@ -10,22 +14,37 @@ function ca_village_hunt:evaluation(cfg, data, filter_own)
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()
local avoid_map = LS.of_pairs(ai.aspects.avoid)
if not villages[1] then
local all_villages, villages = wesnoth.get_villages(), {}
for _,village in ipairs(all_villages) do
if (not avoid_map:get(village[1], village[2])) then
table.insert(villages, village)
end
end
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 }
local n_my_villages, n_allied_villages = 0, 0
for _,village in ipairs(villages) do
local owner = wesnoth.get_village_owner(village[1], village[2]) or -1
if (owner == wesnoth.current.side) then
n_my_villages = n_my_villages + 1
end
if (owner ~= -1) and (not wesnoth.sides.is_enemy(owner, wesnoth.current.side)) then
n_allied_villages = n_allied_villages + 1
end
end
if #my_villages > #villages / #wesnoth.sides then
if (n_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 (n_allied_villages == #villages) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
@ -36,7 +55,32 @@ function ca_village_hunt:evaluation(cfg, data, filter_own)
{ "and", filter_own }
}, true)
if not units[1] then
if (not units[1]) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
local avoid_map = AH.get_avoid_map(ai, nil, true)
VH_unit = nil
for _,unit in ipairs(units) do
local best_cost = 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 = AH.find_path_with_avoid(unit, v[1], v[2], avoid_map)
if (cost < best_cost) then
local dst = AH.next_hop(unit, nil, nil, { path = path, avoid_map = avoid_map })
if (dst[1] ~= unit.x) or (dst[2] ~= unit.y) then
best_cost = cost
VH_unit = unit
VH_dst = dst
end
end
end
end
end
if (not VH_unit) then
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
return 0
end
@ -46,31 +90,10 @@ function ca_village_hunt:evaluation(cfg, data, filter_own)
end
function ca_village_hunt:execution(cfg, data, filter_own)
local unit = AH.get_units_with_moves({
side = wesnoth.current.side,
canrecruit = 'no',
{ "and", filter_own }
}, true)[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
AH.robust_move_and_attack(ai, VH_unit, VH_dst)
VH_unit, VH_dst = nil, nil
end
return ca_village_hunt

View file

@ -928,7 +928,7 @@ return {
data.castle.assigned_villages_x = {}
data.castle.assigned_villages_y = {}
if not ai.aspects.passive_leader and (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(leader)) 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

@ -12,7 +12,16 @@ 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 = unit:defense(wesnoth.get_terrain(unit.x, unit.y))/15
-- Take caution into account here. We want the multiplier to be:
-- 1 for default caution (0.25)
-- 0 for minimal caution <= 0
-- 2 for caution = 1
local caution_factor = ai.aspects.caution
if (caution_factor < 0) then caution_factor = 0 end
caution_factor = math.sqrt(caution_factor) * 2
local hp_per_level = unit:defense(wesnoth.get_terrain(unit.x, unit.y))/15 * caution_factor
local level = unit.level
-- Leaders are considered to be higher level because of their value
@ -23,9 +32,13 @@ function retreat_functions.min_hp(unit)
-- Account for poison damage on next turn
if unit.status.poisoned then min_hp = min_hp + wesnoth.game_config.poison_amount end
-- Make sure that units are actually injured
if min_hp > unit.max_hitpoints - 4 then
min_hp = unit.max_hitpoints - 4
-- Make sure that units are actually injured (only relevant for low-HP units)
-- Want this to be roughly half the units HP at caution=0, close to full HP at caution=1
local hp_factor = 0.5 + 0.25 * caution_factor
if (hp_factor > 1) then hp_factor = 1 end
local max_min_hp = (unit.max_hitpoints - 4) * hp_factor
if (min_hp > max_min_hp) then
min_hp = max_min_hp
end
return min_hp
@ -33,11 +46,13 @@ end
-- Given a set of units, return one from the set that should retreat and the location to retreat to
-- Return nil if no unit needs to retreat
function retreat_functions.retreat_injured_units(units)
function retreat_functions.retreat_injured_units(units, avoid_map)
-- Split units into those that regenerate and those that do not
local regen, regen_amounts, non_regen = {}, {}, {}
for i,u in ipairs(units) do
if u.hitpoints < retreat_functions.min_hp(u) then
if (u.hitpoints < retreat_functions.min_hp(u))
and ((not u.canrecruit) or (not ai.aspects.passive_leader))
then
if u:ability('regenerate') then
-- Find the best regeneration ability and use it to estimate hp regained by regeneration
local abilities = wml.get_child(u.__cfg, "abilities")
@ -60,7 +75,7 @@ function retreat_functions.retreat_injured_units(units)
-- First we retreat non-regenerating units to healing terrain, if they can get to a safe location
local unit_nr, loc_nr, threat_nr
if non_regen[1] then
unit_nr, loc_nr, threat_nr = retreat_functions.get_retreat_injured_units(non_regen, {})
unit_nr, loc_nr, threat_nr = retreat_functions.get_retreat_injured_units(non_regen, {}, avoid_map)
if unit_nr and (threat_nr == 0) then
return unit_nr, loc_nr, threat_nr
end
@ -69,7 +84,7 @@ function retreat_functions.retreat_injured_units(units)
-- Then we retreat regenerating units to terrain with high defense, if they can get to a safe location
local unit_r, loc_r, threat_r
if regen[1] then
unit_r, loc_r, threat_r = retreat_functions.get_retreat_injured_units(regen, regen_amounts)
unit_r, loc_r, threat_r = retreat_functions.get_retreat_injured_units(regen, regen_amounts, avoid_map)
if unit_r and (threat_r == 0) then
return unit_r, loc_r, threat_r
end
@ -118,7 +133,7 @@ function retreat_functions.get_healing_locations()
return healing_locs
end
function retreat_functions.get_retreat_injured_units(healees, regen_amounts)
function retreat_functions.get_retreat_injured_units(healees, regen_amounts, avoid_map)
-- Only retreat to safe locations
local enemies = AH.get_attackable_enemies()
local enemy_attack_map = BC.get_attack_map(enemies)
@ -134,20 +149,22 @@ function retreat_functions.get_retreat_injured_units(healees, regen_amounts)
-- Unit cannot self heal, make the terrain do it for us if possible
local location_subset = {}
for j,loc in ipairs(possible_locations) do
local heal_amount = wesnoth.get_terrain_info(wesnoth.get_terrain(loc[1], loc[2])).healing or 0
if heal_amount == true then
-- handle deprecated syntax
-- TODO: remove this when removed from game
heal_amount = 8
if (not avoid_map) or (not avoid_map:get(loc[1], loc[2])) then
local heal_amount = wesnoth.get_terrain_info(wesnoth.get_terrain(loc[1], loc[2])).healing or 0
if heal_amount == true then
-- handle deprecated syntax
-- TODO: remove this when removed from game
heal_amount = 8
end
local curing = 0
if heal_amount > 0 then
curing = 2
end
local healer_values = healing_locs:get(loc[1], loc[2]) or {0, 0}
heal_amount = math.max(heal_amount, healer_values[1])
curing = math.max(curing, healer_values[2])
table.insert(location_subset, {loc[1], loc[2], heal_amount, curing})
end
local curing = 0
if heal_amount > 0 then
curing = 2
end
local healer_values = healing_locs:get(loc[1], loc[2]) or {0, 0}
heal_amount = math.max(heal_amount, healer_values[1])
curing = math.max(curing, healer_values[2])
table.insert(location_subset, {loc[1], loc[2], heal_amount, curing})
end
possible_locations = location_subset

View file

@ -19,7 +19,10 @@ function ca_healer_move:evaluation(cfg, data)
local healers, healers_noMP = {}, {}
for _,healer in ipairs(all_healers) do
if (healer.moves > 0) then
-- For the purpose of this evaluation, guardians count as units without moves, as do passive leaders
if (healer.moves > 0) and (not healer.status.guardian)
and ((not healer.canrecruit) or (not ai.aspects.passive_leader))
then
table.insert(healers, healer)
else
table.insert(healers_noMP, healer)

View file

@ -15,6 +15,46 @@ function wesnoth.micro_ais.fast_ai(cfg)
-- Also need to delete/add some default CAs
if (cfg.action == 'delete') then
-- This can be done independently of whether these were removed earlier
wesnoth.sides.add_ai_component(cfg.side, "stage[main_loop].candidate_action",
{
id="castle_switch",
engine="lua",
name="ai_default_rca::castle_switch",
max_score=195000,
location="ai/lua/ca_castle_switch.lua"
}
)
wesnoth.sides.add_ai_component(cfg.side, "stage[main_loop].candidate_action",
{
id="retreat_injured",
engine="lua",
name="ai_default_rca::retreat_injured",
max_score=192000,
location="ai/lua/ca_retreat_injured.lua"
}
)
wesnoth.sides.add_ai_component(cfg.side, "stage[main_loop].candidate_action",
{
id="spread_poison",
engine="lua",
name="ai_default_rca::spread_poison",
max_score=190000,
location="ai/lua/ca_spread_poison.lua"
}
)
wesnoth.sides.add_ai_component(cfg.side, "stage[main_loop].candidate_action",
{
id="high_xp_attack",
engine="lua",
name="ai_default_rca::high_xp_attack",
location="ai/lua/ca_high_xp_attack.lua",
max_score=100010
}
)
wesnoth.sides.add_ai_component(cfg.side, "stage[main_loop].candidate_action",
{
id="combat",
@ -25,6 +65,16 @@ function wesnoth.micro_ais.fast_ai(cfg)
}
)
wesnoth.sides.add_ai_component(cfg.side, "stage[main_loop].candidate_action",
{
id="place_healers",
engine="lua",
name="ai_default_rca::place_healers",
max_score=96000,
location="ai/lua/ca_place_healers.lua"
}
)
wesnoth.sides.add_ai_component(cfg.side, "stage[main_loop].candidate_action",
{
id="villages",
@ -56,6 +106,7 @@ function wesnoth.micro_ais.fast_ai(cfg)
)
else
if (not cfg.skip_combat_ca) then
wesnoth.sides.delete_ai_component(cfg.side, "stage[main_loop].candidate_action[spread_poison]")
wesnoth.sides.delete_ai_component(cfg.side, "stage[main_loop].candidate_action[high_xp_attack]")
wesnoth.sides.delete_ai_component(cfg.side, "stage[main_loop].candidate_action[combat]")
else
@ -67,10 +118,11 @@ function wesnoth.micro_ais.fast_ai(cfg)
end
if (not cfg.skip_move_ca) then
wesnoth.sides.delete_ai_component(cfg.side, "stage[main_loop].candidate_action[castle_switch]")
wesnoth.sides.delete_ai_component(cfg.side, "stage[main_loop].candidate_action[retreat_injured]")
wesnoth.sides.delete_ai_component(cfg.side, "stage[main_loop].candidate_action[place_healers]")
wesnoth.sides.delete_ai_component(cfg.side, "stage[main_loop].candidate_action[villages]")
wesnoth.sides.delete_ai_component(cfg.side, "stage[main_loop].candidate_action[retreat]")
wesnoth.sides.delete_ai_component(cfg.side, "stage[main_loop].candidate_action[move_to_targets]")
else
for i,parm in ipairs(CA_parms) do

View file

@ -257,7 +257,6 @@ This is the story of Kalenz, Landar, and of the Elves in the first days of the h
#endif
{AI_SIMPLE_DAY_ASPECT caution 0}
{MODIFY_AI_ADD_CANDIDATE_ACTION 4 main_loop {AI_CA_POISONING}}
[/ai]
[/side]

View file

@ -283,7 +283,6 @@
{AI_SIMPLE_NIGHT_ASPECT grouping offensive}
{AI_SIMPLE_ALWAYS_ASPECT villages_per_scout 5}
{AI_SIMPLE_ALWAYS_ASPECT recruitment_pattern "scout,scout,scout,fighter,archer,mixed fighter"}
{MODIFY_AI_ADD_CANDIDATE_ACTION 4 main_loop {AI_CA_POISONING}}
[/ai]
[/side]
#### /Side4 code ####
@ -319,7 +318,6 @@
{AI_SIMPLE_NIGHT_ASPECT grouping offensive}
{AI_SIMPLE_ALWAYS_ASPECT villages_per_scout 5}
{AI_SIMPLE_ALWAYS_ASPECT recruitment_pattern "scout,fighter,fighter,archer,mixed fighter"}
{MODIFY_AI_ADD_CANDIDATE_ACTION 5 main_loop {AI_CA_POISONING}}
[/ai]
[/side]

View file

@ -1,29 +0,0 @@
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 = 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
retreat_unit = unit
retreat_dst = dst
return 101000
end
return 0
end
function retreat:execution(cfg, data)
AH.movefull_outofway_stopunit(ai, retreat_unit, retreat_dst[1], retreat_dst[2])
retreat_unit, retreat_dst = nil, nil
end
return retreat

View file

@ -74,15 +74,6 @@
[/facet]
[/aspect]
{MODIFY_AI_ADD_CANDIDATE_ACTION 2 main_loop (
[candidate_action]
engine=lua
name=retreat
id=retreat
max_score=101000
location="campaigns/The_Rise_Of_Wesnoth/ai/ca_retreat.lua"
[/candidate_action]
)}
{MODIFY_AI_ADD_CANDIDATE_ACTION 2 main_loop (
[candidate_action]
engine=lua
@ -92,6 +83,7 @@
location="campaigns/The_Rise_Of_Wesnoth/ai/ca_aggressive_attack_no_suicide.lua"
[/candidate_action]
)}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop spread_poison}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop high_xp_attack}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 2 main_loop combat}
[/ai]
@ -135,15 +127,6 @@
[/facet]
[/aspect]
{MODIFY_AI_ADD_CANDIDATE_ACTION 3 main_loop (
[candidate_action]
engine=lua
name=retreat
id=retreat
max_score=101000
location="campaigns/The_Rise_Of_Wesnoth/ai/ca_retreat.lua"
[/candidate_action]
)}
{MODIFY_AI_ADD_CANDIDATE_ACTION 3 main_loop (
[candidate_action]
engine=lua
@ -153,6 +136,7 @@
location="campaigns/The_Rise_Of_Wesnoth/ai/ca_aggressive_attack_no_suicide.lua"
[/candidate_action]
)}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 3 main_loop spread_poison}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 3 main_loop high_xp_attack}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 3 main_loop combat}
[/ai]
@ -196,15 +180,6 @@
[/facet]
[/aspect]
{MODIFY_AI_ADD_CANDIDATE_ACTION 4 main_loop (
[candidate_action]
engine=lua
name=retreat
id=retreat
max_score=101000
location="campaigns/The_Rise_Of_Wesnoth/ai/ca_retreat.lua"
[/candidate_action]
)}
{MODIFY_AI_ADD_CANDIDATE_ACTION 4 main_loop (
[candidate_action]
engine=lua
@ -214,6 +189,7 @@
location="campaigns/The_Rise_Of_Wesnoth/ai/ca_aggressive_attack_no_suicide.lua"
[/candidate_action]
)}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 4 main_loop spread_poison}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 4 main_loop high_xp_attack}
{MODIFY_AI_DELETE_CANDIDATE_ACTION 4 main_loop combat}
[/ai]

View file

@ -521,77 +521,39 @@
#define EXPERIMENTAL_AI
#deprecated 2 1.15 Use ai_algorithm=experimental_ai in the [ai] tag instead
# Experimental AI with alternative recruitment, castle switching, alternative retreating,
# village grabbing, poison spreading, healer placement, village hunting and
# move-to-enemy candidate actions.
# Note: The Experimental AI and the default AI are currently almost identical.
# This is a placeholder for future development.
# Put this into the [side][ai] tag.
# Does not work in [modify_side][ai] or [modify_ai] at the moment.
[stage]
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
{AI_CA_GOTO}
{AI_CA_RECRUIT_RUSHERS}
{AI_CA_CASTLE_SWITCH}
{AI_CA_RETREAT_INJURED}
{AI_CA_GRAB_VILLAGES}
{AI_CA_SPREAD_POISON}
#{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_HIGH_XP_ATTACK}
{AI_CA_COMBAT}
{AI_CA_PLACE_HEALERS}
{AI_CA_HEALING}
{AI_CA_VILLAGES}
{AI_CA_RETREAT}
#{AI_CA_VILLAGES}
{AI_CA_MOVE_TO_TARGETS}
{AI_CA_LEADER_SHARES_KEEP}
[candidate_action]
engine=lua
name=recruit_rushers
max_score=196000
location="ai/lua/ca_recruit_rushers.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=switch_castle
max_score=195000
location="ai/lua/ca_castle_switch.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=retreat_injured
max_score=192000
location="ai/lua/ca_retreat_injured.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=grab_villages
max_score=191000
location="ai/lua/ca_grab_villages.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=spread_poison
max_score=190000
location="ai/lua/ca_spread_poison.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=place_healers
max_score=96000
location="ai/lua/ca_place_healers.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=village_hunt
max_score=30000
location="ai/lua/ca_village_hunt.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=move_to_any_enemy
max_score=1000
location="ai/lua/ca_move_to_any_enemy.lua"
[/candidate_action]
{AI_CA_MOVE_TO_ANY_ENEMY}
[/stage]
#enddef
#define CUSTOMIZABLE_EXPERIMENTAL_AI ARGS
# Note: The Experimental AI and the default AI are currently almost identical.
# However, this macro allows the use of custom parameters.
# Use the Experimental AI with custom parameter setting
# Put this into the [side][ai] tag.
# Does not work in [modify_side][ai] or [modify_ai] at the moment.
@ -599,18 +561,9 @@
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
{AI_CA_GOTO}
#{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_HIGH_XP_ATTACK}
{AI_CA_COMBAT}
{AI_CA_HEALING}
{AI_CA_VILLAGES}
{AI_CA_RETREAT}
{AI_CA_MOVE_TO_TARGETS}
{AI_CA_LEADER_SHARES_KEEP}
[candidate_action]
engine=lua
id=recruit_rushers
name=recruit_rushers
max_score=196000
location="ai/lua/ca_recruit_rushers.lua"
@ -618,47 +571,20 @@
{ARGS}
[/args]
[/candidate_action]
[candidate_action]
engine=lua
name=switch_castle
max_score=195000
location="ai/lua/ca_castle_switch.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=retreat_injured
max_score=192000
location="ai/lua/ca_retreat_injured.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=grab_villages
max_score=191000
location="ai/lua/ca_grab_villages.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=spread_poison
max_score=190000
location="ai/lua/ca_spread_poison.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=place_healers
max_score=96000
location="ai/lua/ca_place_healers.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=village_hunt
max_score=30000
location="ai/lua/ca_village_hunt.lua"
[/candidate_action]
[candidate_action]
engine=lua
name=move_to_any_enemy
max_score=1000
location="ai/lua/ca_move_to_any_enemy.lua"
[/candidate_action]
{AI_CA_CASTLE_SWITCH}
{AI_CA_RETREAT_INJURED}
{AI_CA_GRAB_VILLAGES}
{AI_CA_SPREAD_POISON}
#{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_HIGH_XP_ATTACK}
{AI_CA_COMBAT}
{AI_CA_PLACE_HEALERS}
{AI_CA_HEALING}
#{AI_CA_VILLAGES}
{AI_CA_MOVE_TO_TARGETS}
{AI_CA_LEADER_SHARES_KEEP}
{AI_CA_MOVE_TO_ANY_ENEMY}
[/stage]
#enddef

View file

@ -6,6 +6,26 @@
200000
#enddef
#define AI_CA_RECRUIT_RUSHERS_SCORE
196000
#enddef
#define AI_CA_CASTLE_SWITCH_SCORE
195000
#enddef
#define AI_CA_RETREAT_INJURED_SCORE
192000
#enddef
#define AI_CA_GRAB_VILLAGES_SCORE
191000
#enddef
#define AI_CA_SPREAD_POISON_SCORE
190000
#enddef
#define AI_CA_RECRUITMENT_SCORE
180000
#enddef
@ -26,6 +46,10 @@
100000
#enddef
#define AI_CA_PLACE_HEALERS_SCORE
96000
#enddef
#define AI_CA_HEALING_SCORE
80000
#enddef
@ -38,6 +62,10 @@
40000
#enddef
#define AI_CA_VILLAGE_HUNT_SCORE
30000
#enddef
#define AI_CA_MOVE_TO_TARGETS_SCORE
20000
#enddef
@ -46,6 +74,10 @@
10000
#enddef
#define AI_CA_MOVE_TO_ANY_ENEMY_SCORE
1000
#enddef
# Keep for backward compatibility
#define AI_CA_PASSIVE_LEADER_SHARES_KEEP_SCORE
{AI_CA_LEADER_SHARES_KEEP_SCORE}
@ -64,6 +96,61 @@
[/candidate_action]
#enddef
#define AI_CA_RECRUIT_RUSHERS
# RCA AI candidate action: recruit_rushers
[candidate_action]
id=recruit_rushers
engine=lua
name=recruit_rushers
max_score={AI_CA_RECRUIT_RUSHERS_SCORE}
location="ai/lua/ca_recruit_rushers.lua"
[/candidate_action]
#enddef
#define AI_CA_CASTLE_SWITCH
# RCA AI candidate action: castle_switch
[candidate_action]
id=castle_switch
engine=lua
name=ai_default_rca::castle_switch
max_score={AI_CA_CASTLE_SWITCH_SCORE}
location="ai/lua/ca_castle_switch.lua"
[/candidate_action]
#enddef
#define AI_CA_RETREAT_INJURED
# RCA AI candidate action: retreat_injured
[candidate_action]
id=retreat_injured
engine=lua
name=ai_default_rca::retreat_injured
max_score={AI_CA_RETREAT_INJURED_SCORE}
location="ai/lua/ca_retreat_injured.lua"
[/candidate_action]
#enddef
#define AI_CA_GRAB_VILLAGES
# RCA AI candidate action: grab_villages
[candidate_action]
id=grab_villages
engine=lua
name=grab_villages
max_score={AI_CA_GRAB_VILLAGES_SCORE}
location="ai/lua/ca_grab_villages.lua"
[/candidate_action]
#enddef
#define AI_CA_SPREAD_POISON
# RCA AI candidate action: spread_poison
[candidate_action]
id=spread_poison
engine=lua
name=ai_default_rca::spread_poison
max_score={AI_CA_SPREAD_POISON_SCORE}
location="ai/lua/ca_spread_poison.lua"
[/candidate_action]
#enddef
#define AI_CA_RECRUITMENT
# RCA AI candidate action: recruitment
[candidate_action]
@ -118,6 +205,17 @@
[/candidate_action]
#enddef
#define AI_CA_PLACE_HEALERS
# RCA AI candidate action: place_healers
[candidate_action]
id=place_healers
engine=lua
name=ai_default_rca::place_healers
max_score={AI_CA_PLACE_HEALERS_SCORE}
location="ai/lua/ca_place_healers.lua"
[/candidate_action]
#enddef
#define AI_CA_HEALING
# RCA AI candidate action: healing
[candidate_action]
@ -151,6 +249,17 @@
[/candidate_action]
#enddef
#define AI_CA_VILLAGE_HUNT
# RCA AI candidate action: village_hunt
[candidate_action]
id=village_hunt
engine=lua
name=village_hunt
max_score={AI_CA_VILLAGE_HUNT_SCORE}
location="ai/lua/ca_village_hunt.lua"
[/candidate_action]
#enddef
#define AI_CA_MOVE_TO_TARGETS
# RCA AI candidate action: move_to_targets
[candidate_action]
@ -173,6 +282,17 @@
[/candidate_action]
#enddef
#define AI_CA_MOVE_TO_ANY_ENEMY
# RCA AI candidate action: move_to_any_enemy
[candidate_action]
id=move_to_any_enemy
engine=lua
name=ai_default_rca::move_to_any_enemy
max_score={AI_CA_MOVE_TO_ANY_ENEMY_SCORE}
location="ai/lua/ca_move_to_any_enemy.lua"
[/candidate_action]
#enddef
# Keep for backward compatibility
#define AI_CA_PASSIVE_LEADER_SHARES_KEEP
{AI_CA_LEADER_SHARES_KEEP}