mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-28 10:38:13 +00:00

I had not realized that TextWrangler changes spaces to tabs when shifting blocks of text to the left, which had been done for all CA files when converting the engine files.
614 lines
27 KiB
Lua
614 lines
27 KiB
Lua
return {
|
|
init = function(ai)
|
|
|
|
-- Grab a useful separate CA as a starting point
|
|
local generic_rush = wesnoth.require("ai/lua/move_to_any_target.lua").init(ai)
|
|
|
|
-- More generic grunt rush (and can, in fact, be used with other unit types as well)
|
|
|
|
local H = wesnoth.require "lua/helper.lua"
|
|
local W = H.set_wml_action_metatable {}
|
|
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
|
local BC = wesnoth.require "ai/lua/battle_calcs.lua"
|
|
local LS = wesnoth.require "lua/location_set.lua"
|
|
local HS = wesnoth.require "ai/micro_ais/cas/ca_healer_move.lua"
|
|
local R = wesnoth.require "ai/lua/retreat.lua"
|
|
|
|
local function print_time(...)
|
|
if generic_rush.data.turn_start_time then
|
|
AH.print_ts_delta(generic_rush.data.turn_start_time, ...)
|
|
else
|
|
AH.print_ts(...)
|
|
end
|
|
end
|
|
|
|
------ Stats at beginning of turn -----------
|
|
|
|
-- This will be blacklisted after first execution each turn
|
|
function generic_rush:stats_eval()
|
|
local score = 999999
|
|
return score
|
|
end
|
|
|
|
function generic_rush:stats_exec()
|
|
local tod = wesnoth.get_time_of_day()
|
|
AH.print_ts(' Beginning of Turn ' .. wesnoth.current.turn .. ' (' .. tod.name ..') stats')
|
|
generic_rush.data.turn_start_time = wesnoth.get_time_stamp() / 1000.
|
|
|
|
for i,s in ipairs(wesnoth.sides) do
|
|
local total_hp = 0
|
|
local units = AH.get_live_units { side = s.side }
|
|
for i,u in ipairs(units) do total_hp = total_hp + u.hitpoints end
|
|
local leader = wesnoth.get_units { side = s.side, canrecruit = 'yes' }[1]
|
|
if leader then
|
|
print(' Player ' .. s.side .. ' (' .. leader.type .. '): ' .. #units .. ' Units with total HP: ' .. total_hp)
|
|
end
|
|
end
|
|
end
|
|
|
|
------- Recruit CA --------------
|
|
|
|
local params = {
|
|
score_function = (function() return 300000 end),
|
|
min_turn_1_recruit = (function() return generic_rush:castle_switch_eval() > 0 end),
|
|
leader_takes_village = (function()
|
|
if generic_rush:castle_switch_eval() > 0 then
|
|
local take_village = #(wesnoth.get_villages {
|
|
x = generic_rush.data.leader_target[1],
|
|
y = generic_rush.data.leader_target[2]
|
|
}) > 0
|
|
return take_village
|
|
end
|
|
return true
|
|
end
|
|
)
|
|
}
|
|
wesnoth.require("ai/lua/generic_recruit_engine.lua").init(ai, generic_rush, params)
|
|
|
|
-------- Castle Switch CA --------------
|
|
local function get_reachable_enemy_leaders(unit)
|
|
local potential_enemy_leaders = AH.get_live_units { canrecruit = 'yes',
|
|
{ "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)
|
|
end
|
|
end
|
|
|
|
return enemy_leaders
|
|
end
|
|
|
|
function generic_rush:castle_switch_eval()
|
|
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'castle_switch'
|
|
if AH.print_eval() then print_time(' - Evaluating castle_switch CA:') end
|
|
|
|
if ai.get_passive_leader() then
|
|
-- Turn off this CA if the leader is passive
|
|
return 0
|
|
end
|
|
|
|
local leader = wesnoth.get_units {
|
|
side = wesnoth.current.side,
|
|
canrecruit = 'yes',
|
|
formula = '($this_unit.moves = $this_unit.max_moves) and ($this_unit.hitpoints = $this_unit.max_hitpoints)'
|
|
}[1]
|
|
if not leader then
|
|
-- CA is irrelevant if no leader or the leader may have moved from another CA
|
|
self.data.leader_target = 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()
|
|
|
|
if self.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, self.data.leader_target[1], self.data.leader_target[2])
|
|
if next_hop and next_hop[1] == self.data.leader_target[1]
|
|
and next_hop[2] == self.data.leader_target[2] then
|
|
return self.data.leader_score
|
|
end
|
|
end
|
|
|
|
local width,height,border = wesnoth.get_map_size()
|
|
local keeps = wesnoth.get_locations {
|
|
terrain = 'K*,K*^*,*^K*', -- Keeps
|
|
x = '1-'..width,
|
|
y = '1-'..height,
|
|
{ "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 / H.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
|
|
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 / (H.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, best_loc[1], best_loc[2])
|
|
|
|
if next_hop and ((next_hop[1] ~= leader.x) or (next_hop[2] ~= leader.y)) then
|
|
-- See if there is a nearby village that can be captured without delaying progress
|
|
local close_villages = wesnoth.get_villages( {
|
|
{ "and", { x = next_hop[1], y = next_hop[2], radius = 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 = wesnoth.copy_unit(leader)
|
|
dummy_leader.x = loc[1]
|
|
dummy_leader.y = loc[2]
|
|
local path_keep, cost_keep = wesnoth.find_path(dummy_leader, best_loc[1], best_loc[2])
|
|
local turns_from_keep = math.ceil(cost_keep/leader.max_moves)
|
|
if turns_from_keep < best_turns
|
|
or (turns_from_keep == 1 and wesnoth.sides[wesnoth.current.side].gold < cheapest_unit_cost)
|
|
then
|
|
-- There is, go there instead
|
|
next_hop = loc
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
self.data.leader_target = next_hop
|
|
|
|
-- if we're on a keep, wait until there are no movable units on the castle before moving off
|
|
self.data.leader_score = 290000
|
|
if wesnoth.get_terrain_info(wesnoth.get_terrain(leader.x, leader.y)).keep then
|
|
local castle = wesnoth.get_locations {
|
|
x = "1-"..width, y = "1-"..height,
|
|
{ "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.get_unit(loc[1], loc[2])
|
|
if not unit then
|
|
should_wait = false
|
|
break
|
|
elseif unit.moves > 0 then
|
|
should_wait = true
|
|
end
|
|
end
|
|
if should_wait then
|
|
self.data.leader_score = 15000
|
|
end
|
|
end
|
|
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return self.data.leader_score
|
|
end
|
|
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 0
|
|
end
|
|
|
|
function generic_rush:castle_switch_exec()
|
|
local leader = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[1]
|
|
|
|
if AH.print_exec() then print_time(' Executing castle_switch CA') end
|
|
if AH.show_messages() then W.message { speaker = leader.id, message = 'Switching castles' } end
|
|
|
|
ai.move(leader, self.data.leader_target[1], self.data.leader_target[2])
|
|
self.data.leader_target = nil
|
|
end
|
|
|
|
------- Grab Villages CA --------------
|
|
|
|
function generic_rush:grab_villages_eval()
|
|
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'grab_villages'
|
|
if AH.print_eval() then print_time(' - Evaluating grab_villages CA:') end
|
|
|
|
-- Check if there are units with moves left
|
|
local units = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'no',
|
|
formula = '$this_unit.moves > 0'
|
|
}
|
|
if (not units[1]) then
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 0
|
|
end
|
|
|
|
local enemies = AH.get_live_units {
|
|
{ "filter_side", {{"enemy_of", {side = wesnoth.current.side} }} }
|
|
}
|
|
|
|
local villages = wesnoth.get_villages()
|
|
-- Just in case:
|
|
if (not villages[1]) then
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 0
|
|
end
|
|
--print('#units, #enemies', #units, #enemies)
|
|
|
|
-- First check if attacks are possible for any unit
|
|
local return_value = 200000
|
|
-- If one with > 50% chance of kill is possible, set return_value to lower than combat CA
|
|
local attacks = ai.get_attacks()
|
|
--print(#attacks)
|
|
for i,a in ipairs(attacks) do
|
|
if (#a.movements == 1) and (a.chance_to_kill > 0.5) then
|
|
return_value = 90000
|
|
break
|
|
end
|
|
end
|
|
|
|
-- Also find which locations can be attacked by enemies
|
|
local enemy_attack_map = BC.get_attack_map(enemies).units
|
|
|
|
-- Now we go through the villages and units
|
|
local max_rating, best_village, best_unit = -9e99, {}, {}
|
|
local village_ratings = {}
|
|
for j,v in ipairs(villages) do
|
|
-- First collect all information that only depends on the village
|
|
local village_rating = 0 -- This is the unit independent rating
|
|
|
|
local unit_in_way = wesnoth.get_unit(v[1], v[2])
|
|
|
|
-- If an enemy can get within one move of the village, we want to hold it
|
|
if enemy_attack_map:get(v[1], v[2]) then
|
|
--print(' within enemy reach', v[1], v[2])
|
|
village_rating = village_rating + 100
|
|
end
|
|
|
|
-- Unowned and enemy-owned villages get a large bonus
|
|
local owner = wesnoth.get_village_owner(v[1], v[2])
|
|
if (not owner) then
|
|
village_rating = village_rating + 10000
|
|
else
|
|
if wesnoth.is_enemy(owner, wesnoth.current.side) then village_rating = village_rating + 20000 end
|
|
end
|
|
|
|
local enemy_distance_from_village = AH.get_closest_enemy(v)
|
|
|
|
-- Now we go on to the unit-dependent rating
|
|
local best_unit_rating = -9e99
|
|
local reachable = false
|
|
for i,u in ipairs(units) do
|
|
-- Skip villages that have units other than 'u' itself on them
|
|
local village_occupied = false
|
|
if unit_in_way and ((unit_in_way.x ~= u.x) or (unit_in_way.y ~= u.y)) then
|
|
village_occupied = true
|
|
end
|
|
|
|
-- Rate all villages that can be reached and are unoccupied by other units
|
|
if (not village_occupied) then
|
|
-- Path finding is expensive, so we do a first cut simply by distance
|
|
-- There is no way a unit can get to the village if the distance is greater than its moves
|
|
local dist = H.distance_between(u.x, u.y, v[1], v[2])
|
|
if (dist <= u.moves) then
|
|
local path, cost = wesnoth.find_path(u, v[1], v[2])
|
|
if (cost <= u.moves) then
|
|
village_rating = village_rating - 1
|
|
reachable = true
|
|
--print('Can reach:', u.id, v[1], v[2], cost)
|
|
local rating = 0
|
|
|
|
-- Prefer strong units if enemies can reach the village, injured units otherwise
|
|
if enemy_attack_map:get(v[1], v[2]) then
|
|
rating = rating + u.hitpoints
|
|
else
|
|
rating = rating + u.max_hitpoints - u.hitpoints
|
|
end
|
|
|
|
-- Prefer not backtracking and moving more distant units to capture villages
|
|
local enemy_distance_from_unit = AH.get_closest_enemy({u.x, u.y})
|
|
rating = rating - (enemy_distance_from_village + enemy_distance_from_unit)/5
|
|
|
|
if (rating > best_unit_rating) then
|
|
best_unit_rating, best_unit = rating, u
|
|
end
|
|
--print(' rating:', rating)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
village_ratings[v] = {village_rating, best_unit, reachable}
|
|
end
|
|
for j,v in ipairs(villages) do
|
|
local rating = village_ratings[v][1]
|
|
if village_ratings[v][3] and rating > max_rating then
|
|
max_rating, best_village, best_unit = rating, v, village_ratings[v][2]
|
|
end
|
|
end
|
|
--print('max_rating', max_rating)
|
|
|
|
if (max_rating > -9e99) then
|
|
self.data.unit, self.data.village = best_unit, best_village
|
|
if (max_rating >= 1000) then
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return return_value
|
|
else
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 0
|
|
end
|
|
end
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 0
|
|
end
|
|
|
|
function generic_rush:grab_villages_exec()
|
|
if AH.print_exec() then print_time(' Executing grab_villages CA') end
|
|
if AH.show_messages() then W.message { speaker = self.data.unit.id, message = 'Grab villages' } end
|
|
|
|
AH.movefull_stopunit(ai, self.data.unit, self.data.village)
|
|
self.data.unit, self.data.village = nil, nil
|
|
end
|
|
|
|
------- Spread Poison CA --------------
|
|
|
|
function generic_rush:spread_poison_eval()
|
|
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'spread_poison'
|
|
if AH.print_eval() then print_time(' - 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_live_units { side = wesnoth.current.side,
|
|
formula = '$this_unit.attacks_left > 0',
|
|
{ "filter_wml", {
|
|
{ "attack", {
|
|
{ "specials", {
|
|
{ "poison", { } }
|
|
} }
|
|
} }
|
|
} },
|
|
canrecruit = 'no'
|
|
}
|
|
--print('#poisoners', #poisoners)
|
|
if (not poisoners[1]) then
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 0
|
|
end
|
|
|
|
local attacks = AH.get_attacks(poisoners)
|
|
--print('#attacks', #attacks)
|
|
if (not attacks[1]) then
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 0
|
|
end
|
|
|
|
-- Go through all possible attacks with poisoners
|
|
local max_rating, best_attack = -9e99, {}
|
|
for i,a in ipairs(attacks) do
|
|
local attacker = wesnoth.get_unit(a.src.x, a.src.y)
|
|
local defender = wesnoth.get_unit(a.target.x, a.target.y)
|
|
|
|
-- Don't try to poison a unit that cannot be poisoned
|
|
local cant_poison = defender.status.poisoned or defender.status.unpoisonable
|
|
|
|
-- For now, we also simply don't poison units on villages (unless standard combat CA does it)
|
|
local on_village = wesnoth.get_terrain_info(wesnoth.get_terrain(defender.x, defender.y)).village
|
|
|
|
-- Also, poisoning units that would level up through the attack or could level on their turn as a result is very bad
|
|
local about_to_level = defender.max_experience - defender.experience <= (wesnoth.unit_types[attacker.type].level * 2)
|
|
|
|
if (not cant_poison) and (not on_village) and (not about_to_level) then
|
|
-- Strongest enemy gets poisoned first
|
|
local rating = defender.hitpoints
|
|
|
|
-- Always attack enemy leader, if possible
|
|
if defender.canrecruit then rating = rating + 1000 end
|
|
|
|
-- Enemies that can regenerate are not good targets
|
|
if wesnoth.unit_ability(defender, 'regenerate') then rating = rating - 1000 end
|
|
|
|
-- More priority to enemies on strong terrain
|
|
local defender_defense = 100 - wesnoth.unit_defense(defender, wesnoth.get_terrain(defender.x, defender.y))
|
|
rating = rating + defender_defense / 4.
|
|
|
|
-- For the same attacker/defender pair, go to strongest terrain
|
|
local attack_defense = 100 - wesnoth.unit_defense(attacker, wesnoth.get_terrain(a.dst.x, a.dst.y))
|
|
rating = rating + attack_defense / 2.
|
|
--print('rating', rating)
|
|
|
|
-- And from village everything else being equal
|
|
local is_village = wesnoth.get_terrain_info(wesnoth.get_terrain(a.dst.x, a.dst.y)).village
|
|
if is_village then rating = rating + 0.5 end
|
|
|
|
if rating > max_rating then
|
|
max_rating, best_attack = rating, a
|
|
end
|
|
end
|
|
end
|
|
|
|
if (max_rating > -9e99) then
|
|
self.data.attack = best_attack
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 190000
|
|
end
|
|
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end
|
|
return 0
|
|
end
|
|
|
|
function generic_rush:spread_poison_exec()
|
|
local attacker = wesnoth.get_unit(self.data.attack.src.x, self.data.attack.src.y)
|
|
|
|
if AH.print_exec() then print_time(' Executing spread_poison CA') end
|
|
if AH.show_messages() then W.message { speaker = attacker.id, message = 'Poison attack' } end
|
|
|
|
local defender = wesnoth.get_unit(self.data.attack.target.x, self.data.attack.target.y)
|
|
|
|
AH.movefull_stopunit(ai, attacker, self.data.attack.dst.x, self.data.attack.dst.y)
|
|
|
|
-- Find the poison weapon
|
|
-- If several attacks have poison, this will always find the last one
|
|
local is_poisoner, poison_weapon = AH.has_weapon_special(attacker, "poison")
|
|
|
|
ai.attack(attacker, defender, poison_weapon)
|
|
|
|
self.data.attack = nil
|
|
end
|
|
|
|
------- Place Healers CA --------------
|
|
|
|
function generic_rush:place_healers_eval()
|
|
if HS:evaluation(ai, nil, self) > 0 then
|
|
return 95000
|
|
end
|
|
return 0
|
|
end
|
|
|
|
function generic_rush:place_healers_exec()
|
|
HS:execution(ai, nil, self)
|
|
end
|
|
|
|
------- Retreat CA --------------
|
|
|
|
function generic_rush:retreat_injured_units_eval()
|
|
local units = wesnoth.get_units {
|
|
side = wesnoth.current.side,
|
|
formula = '$this_unit.moves > 0'
|
|
}
|
|
local unit, loc = R.retreat_injured_units(units)
|
|
if unit then
|
|
self.data.retreat_unit = unit
|
|
self.data.retreat_loc = loc
|
|
|
|
-- First check if attacks are possible for any unit
|
|
-- If one with > 50% chance of kill is possible, set return_value to lower than combat CA
|
|
local attacks = ai.get_attacks()
|
|
for i,a in ipairs(attacks) do
|
|
if (#a.movements == 1) and (a.chance_to_kill > 0.5) then
|
|
return 95000
|
|
end
|
|
end
|
|
return 205000
|
|
end
|
|
return 0
|
|
end
|
|
|
|
function generic_rush:retreat_injured_units_exec()
|
|
AH.movefull_outofway_stopunit(ai, self.data.retreat_unit, self.data.retreat_loc)
|
|
self.data.retreat_unit = nil
|
|
self.data.retreat_loc = nil
|
|
end
|
|
|
|
------- 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
|
|
|
|
function generic_rush:village_hunt_eval()
|
|
local villages = wesnoth.get_villages()
|
|
|
|
if not villages[1] then
|
|
return 0
|
|
end
|
|
|
|
local my_villages = wesnoth.get_villages { owner_side = wesnoth.current.side }
|
|
|
|
if #my_villages > #villages / #wesnoth.sides then
|
|
return 0
|
|
end
|
|
|
|
local allied_villages = wesnoth.get_villages { {"filter_owner", { {"ally_of", { side = wesnoth.current.side }} }} }
|
|
if #allied_villages == #villages then
|
|
return 0
|
|
end
|
|
|
|
local units = wesnoth.get_units {
|
|
side = wesnoth.current.side,
|
|
canrecruit = false,
|
|
formula = '$this_unit.moves > 0'
|
|
}
|
|
|
|
if not units[1] then
|
|
return 0
|
|
end
|
|
|
|
return 30000
|
|
end
|
|
|
|
function generic_rush:village_hunt_exec()
|
|
local unit = wesnoth.get_units({
|
|
side = wesnoth.current.side,
|
|
canrecruit = false,
|
|
formula = '$this_unit.moves > 0'
|
|
})[1]
|
|
|
|
local villages = wesnoth.get_villages()
|
|
local target, best_cost = nil, 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)
|
|
ai.move(unit, dest[1], dest[2])
|
|
end
|
|
end
|
|
|
|
return generic_rush
|
|
end
|
|
}
|