AI: enable multiple leaders in castle_switch CA
Previously the CA would already move multiple leaders if all leaders were to be moved, but it would abandon moving any leader after finding one that should not move.
This commit is contained in:
parent
64e969af11
commit
ab2c3bfcc2
2 changed files with 134 additions and 125 deletions
|
@ -4,7 +4,7 @@ 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, avoid_map)
|
||||
-- We're cheating a little here and also find hidden enemy leaders. That's
|
||||
|
@ -38,15 +38,16 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
|
|||
return 0
|
||||
end
|
||||
|
||||
local leader = AH.get_units_with_moves({
|
||||
local 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)[1]
|
||||
if not leader then
|
||||
}, true)
|
||||
|
||||
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
|
||||
|
@ -55,142 +56,156 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
|
|||
|
||||
local avoid_map = AH.get_avoid_map(ai, nil, true)
|
||||
|
||||
if data.leader_target and wesnoth.sides[wesnoth.current.side].gold >= cheapest_unit_cost then
|
||||
if data.CS_leader and wesnoth.sides[wesnoth.current.side].gold >= cheapest_unit_cost then
|
||||
-- make sure move is still valid
|
||||
local path, cost = AH.find_path_with_avoid(leader, data.leader_target[1], data.leader_target[2], avoid_map)
|
||||
local next_hop = AH.next_hop(leader, nil, nil, { path = path, avoid_map = avoid_map })
|
||||
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.leader_target = nil
|
||||
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, avoid_map)
|
||||
|
||||
-- Look for the best keep
|
||||
local best_score, best_loc, best_turns, best_path = 0, {}, 3
|
||||
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
|
||||
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
|
||||
best_path = path
|
||||
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, nil, nil, { path = best_path, avoid_map = avoid_map })
|
||||
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 = 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
|
||||
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 = 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
|
||||
|
||||
-- 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
|
||||
elseif unit.moves > 0 then
|
||||
should_wait = true
|
||||
break
|
||||
-- if we're on a keep, wait until there are no movable units on the castle before moving off
|
||||
local 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
|
||||
elseif unit.moves > 0 then
|
||||
should_wait = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if should_wait then
|
||||
leader_score = 15000
|
||||
end
|
||||
end
|
||||
if should_wait then
|
||||
CS_leader_score = 15000
|
||||
|
||||
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
|
||||
|
@ -200,17 +215,11 @@ function ca_castle_switch:evaluation(cfg, data, filter_own)
|
|||
end
|
||||
|
||||
function ca_castle_switch:execution(cfg, data, filter_own)
|
||||
local leader = AH.get_units_with_moves({
|
||||
side = wesnoth.current.side,
|
||||
canrecruit = 'yes',
|
||||
{ "and", filter_own }
|
||||
}, true)[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.checked_move(ai, data.CS_leader, data.CS_leader_target[1], data.CS_leader_target[2])
|
||||
data.CS_leader, data.CS_leader_target = nil
|
||||
end
|
||||
|
||||
return ca_castle_switch
|
||||
|
|
|
@ -20,8 +20,8 @@ if ca_castle_switch then
|
|||
params.leader_takes_village = (function()
|
||||
if ca_castle_switch:evaluation({}, dummy_engine.data) > 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
|
||||
|
|
Loading…
Add table
Reference in a new issue