Merge pull request #4600 from mattsc/expai_ca_merge
Merge Experimental AI candidate actions into the default AI
This commit is contained in:
commit
d650a8336d
23 changed files with 955 additions and 516 deletions
|
@ -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]
|
||||
|
|
27
data/ai/ais/ai_default_rca_1_14.cfg
Normal file
27
data/ai/ais/ai_default_rca_1_14.cfg
Normal 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]
|
|
@ -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]
|
35
data/ai/dev/ai_experimental.cfg
Normal file
35
data/ai/dev/ai_experimental.cfg
Normal 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]
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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]
|
||||
|
||||
|
|
|
@ -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
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Add table
Reference in a new issue