
Insert these calls in loops that do expensive calculations for the CAs of the default and experimental AIs.
264 lines
11 KiB
Lua
264 lines
11 KiB
Lua
-------- Castle Switch CA --------------
|
|
|
|
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
|
local M = wesnoth.map
|
|
|
|
local CS_leader_score
|
|
-- Note that CS_leader and CS_leader_target are also needed by the recruiting CA, so they must be stored in 'data'
|
|
|
|
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.
|
|
local potential_enemy_leaders = AH.get_live_units { canrecruit = 'yes',
|
|
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } }
|
|
}
|
|
local enemy_leaders = {}
|
|
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
|
|
wesnoth.interface.handle_user_interact()
|
|
local path, cost = wesnoth.paths.find_path(unit, e.x, e.y, { ignore_units = true, ignore_visibility = true })
|
|
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.terrain_types[wesnoth.current.map[leader]].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, recruiting_leader)
|
|
-- @recruiting_leader is passed from the recuit_rushers CA for the leader_takes_villages evaluation
|
|
-- evaluation. If it is set, we do the castle switch evaluation only for that leader
|
|
|
|
local start_time, ca_name = wesnoth.ms_since_init() / 1000., 'castle_switch'
|
|
if AH.print_eval() then AH.print_ts(' - Evaluating castle_switch CA:') end
|
|
|
|
if ai.aspects.passive_leader then
|
|
-- Turn off this CA if the leader is passive
|
|
return 0
|
|
end
|
|
|
|
local 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 }
|
|
}, true)
|
|
end
|
|
|
|
local non_passive_leader
|
|
for _,l in pairs(leaders) do
|
|
if (not AH.is_passive_leader(ai.aspects.passive_leader, l.id)) then
|
|
non_passive_leader = l
|
|
break
|
|
end
|
|
end
|
|
|
|
if not non_passive_leader then
|
|
-- CA is irrelevant if no leader or the leader may have moved from another CA
|
|
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
|
|
|
|
-- Also need to check that the stored leader has not been killed
|
|
if data.CS_leader and not data.CS_leader.valid then
|
|
data.CS_leader, data.CS_leader_target = nil, nil
|
|
end
|
|
|
|
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
|
|
|
|
-- make sure move is still valid
|
|
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
|
|
|
|
-- Look for the best keep
|
|
local overall_best_score = 0
|
|
for _,leader in ipairs(leaders) do
|
|
local best_score, best_loc, best_turns, best_path = 0, {}, 3, nil
|
|
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, avoid_map)
|
|
|
|
for i,loc in ipairs(keeps) do
|
|
-- Only consider keeps within 2 turns movement
|
|
wesnoth.interface.handle_user_interact()
|
|
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
|
|
|
|
-- 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.terrain_types[wesnoth.current.map[leader]].keep then
|
|
local close_unowned_village = (wesnoth.map.find {
|
|
wml.tag['and']{
|
|
x = leader.x,
|
|
y = leader.y,
|
|
radius = leader.max_moves
|
|
},
|
|
gives_income = true,
|
|
owner_side = 0
|
|
})[1]
|
|
if close_unowned_village then
|
|
local score = 1/best_turns
|
|
for j,e in ipairs(enemy_leaders) do
|
|
-- count all distances as three less than they actually are
|
|
score = score + 1 / (M.distance_between(leader.x, leader.y, e.x, e.y) - 3)
|
|
end
|
|
|
|
if score > best_score then
|
|
best_score = 0
|
|
end
|
|
end
|
|
end
|
|
|
|
if best_score > 0 then
|
|
local next_hop = AH.next_hop(leader, 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.map.find( {
|
|
wml.tag["and"]{ x = next_hop[1], y = next_hop[2], radius = leader.max_moves },
|
|
gives_income = true,
|
|
owner_side = 0 })
|
|
local cheapest_unit_cost = AH.get_cheapest_recruit_cost(leader)
|
|
for i,loc in ipairs(close_villages) do
|
|
wesnoth.interface.handle_user_interact()
|
|
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.paths.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
|
|
|
|
local leader_score = other_units_on_keep(leader)
|
|
best_score = best_score + leader_score
|
|
|
|
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
|
|
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 0
|
|
end
|
|
|
|
function ca_castle_switch:execution(cfg, data, filter_own)
|
|
if AH.print_exec() then AH.print_ts(' Executing castle_switch CA') end
|
|
if AH.show_messages() then wesnoth.wml_actions.message { speaker = data.leader.id, message = 'Switching castles' } end
|
|
|
|
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, nil
|
|
end
|
|
|
|
return ca_castle_switch
|