diff --git a/data/ai/lua/ca_castle_switch.lua b/data/ai/lua/ca_castle_switch.lua new file mode 100644 index 000000000000..6927a2ea67f7 --- /dev/null +++ b/data/ai/lua/ca_castle_switch.lua @@ -0,0 +1,203 @@ +-------- Castle Switch CA -------------- + +local AH = wesnoth.require "ai/lua/ai_helper.lua" +local M = wesnoth.map + +local function get_reachable_enemy_leaders(unit) + -- 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 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 + +local ca_castle_switch = {} + +function ca_castle_switch:evaluation(cfg, data) + 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 + + if ai.aspects.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 = '(movement_left = total_movement) and (hitpoints = max_hitpoints)' + }[1] + if not leader then + -- CA is irrelevant if no leader or the leader may have moved from another CA + 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 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 + return 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 / 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 + 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 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 = 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 + 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 + 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 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 + data.leader_score = 15000 + end + end + + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return data.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) + local leader = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[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 +end + +return ca_castle_switch diff --git a/data/ai/lua/ca_grab_villages.lua b/data/ai/lua/ca_grab_villages.lua new file mode 100644 index 000000000000..09a1d8b33f34 --- /dev/null +++ b/data/ai/lua/ca_grab_villages.lua @@ -0,0 +1,140 @@ +------- Grab Villages CA -------------- + +local AH = wesnoth.require "ai/lua/ai_helper.lua" +local BC = wesnoth.require "ai/lua/battle_calcs.lua" +local M = wesnoth.map + +local ca_grab_villages = {} + +function ca_grab_villages:evaluation(cfg, data) + local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'grab_villages' + if AH.print_eval() then AH.print_ts(' - 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 = 'movement_left > 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_attackable_enemies() + + 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 + + -- 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() + 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 = - math.huge + 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 + 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 = - math.huge + 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 AH.is_visible_unit(wesnoth.current.side, unit_in_way) and ((unit_in_way ~= u)) 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 = M.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 + 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 + 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 + + if best_village then + data.unit, 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 ca_grab_villages:execution(cfg, data) + if AH.print_exec() then AH.print_ts(' Executing grab_villages CA') end + if AH.show_messages() then wesnoth.wml_actions.message { speaker = data.unit.id, message = 'Grab villages' } end + + AH.movefull_stopunit(ai, data.unit, data.village) + data.unit, data.village = nil, nil +end + +return ca_grab_villages diff --git a/data/ai/lua/ca_move_to_any_enemy.lua b/data/ai/lua/ca_move_to_any_enemy.lua new file mode 100644 index 000000000000..aae0afdc2b8d --- /dev/null +++ b/data/ai/lua/ca_move_to_any_enemy.lua @@ -0,0 +1,60 @@ +------- Move To Any Enemy CA -------------- +-- Move AI units toward any enemy on the map. This has a very low CA score and +-- 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 AH = wesnoth.require "ai/lua/ai_helper.lua" + +local ca_move_to_any_enemy = {} + +function ca_move_to_any_enemy:evaluation(cfg, data) + 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 = wesnoth.get_units { + side = wesnoth.current.side, + canrecruit = 'no', + formula = 'movement_left > 0' + } + + if (not units[1]) then + -- No units with moves left + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 0 + end + + local unit, destination + -- Find a unit that has a path to an space close to an enemy + for i,u in ipairs(units) do + local distance, 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 then + break + 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 + + data.destination = destination + data.unit = unit + + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 1 +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, data.unit, data.destination[1], data.destination[2]) +end + +return ca_move_to_any_enemy diff --git a/data/ai/lua/ca_place_healers.lua b/data/ai/lua/ca_place_healers.lua new file mode 100644 index 000000000000..de15dca6904f --- /dev/null +++ b/data/ai/lua/ca_place_healers.lua @@ -0,0 +1,25 @@ +------- Place Healers CA -------------- + +local AH = wesnoth.require "ai/lua/ai_helper.lua" +local HS = wesnoth.require "ai/micro_ais/cas/ca_healer_move.lua" + +local ca_place_healers = {} + +function ca_place_healers:evaluation(cfg, data) + local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'place_healers' + if AH.print_eval() then AH.print_ts(' - Evaluating place_healers CA:') end + + if HS:evaluation(cfg, data) > 0 then + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 95000 + end + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 0 +end + +function ca_place_healers:execution(cfg, data) + if AH.print_exec(cfg, data) then AH.print_ts(' Executing place_healers CA') end + HS:execution() +end + +return ca_place_healers diff --git a/data/ai/lua/ca_recruit_rushers.lua b/data/ai/lua/ca_recruit_rushers.lua new file mode 100644 index 000000000000..afd34ff4d69b --- /dev/null +++ b/data/ai/lua/ca_recruit_rushers.lua @@ -0,0 +1,32 @@ +-- Make the generic_recruit_engine functions work as external CAs + +local dummy_engine = { data = {} } +local ca_castle_switch = wesnoth.require("ai/lua/ca_castle_switch.lua") +local params = { + score_function = (function() return 300000 end), + min_turn_1_recruit = (function() return ca_castle_switch:evaluation({}, dummy_engine.data) > 0 end), + 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] + }) > 0 + return take_village + end + return true + end + ) +} +wesnoth.require("ai/lua/generic_recruit_engine.lua").init(dummy_engine, params) + +local ca_recruit_rushers = {} + +function ca_recruit_rushers:evaluation(cfg, data) + return dummy_engine:recruit_rushers_eval() +end + +function ca_recruit_rushers:execution(cfg, data) + return dummy_engine:recruit_rushers_exec() +end + +return ca_recruit_rushers \ No newline at end of file diff --git a/data/ai/lua/ca_retreat_injured.lua b/data/ai/lua/ca_retreat_injured.lua new file mode 100644 index 000000000000..0932edf09c1b --- /dev/null +++ b/data/ai/lua/ca_retreat_injured.lua @@ -0,0 +1,44 @@ +------- Retreat CA -------------- + +local AH = wesnoth.require "ai/lua/ai_helper.lua" +local R = wesnoth.require "ai/lua/retreat.lua" + +local ca_retreat_injured = {} + +function ca_retreat_injured:evaluation(cfg, data) + 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 + + local units = wesnoth.get_units { + side = wesnoth.current.side, + formula = 'movement_left > 0' + } + local unit, loc = R.retreat_injured_units(units) + if unit then + data.retreat_unit = unit + 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 + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 95000 + end + end + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 205000 + end + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 0 +end + +function ca_retreat_injured:execution(cfg, data) + if AH.print_exec() then AH.print_ts(' Executing retreat_injured CA') end + AH.robust_move_and_attack(ai, data.retreat_unit, data.retreat_loc) + data.retreat_unit = nil + data.retreat_loc = nil +end + +return ca_retreat_injured diff --git a/data/ai/lua/ca_spread_poison.lua b/data/ai/lua/ca_spread_poison.lua new file mode 100644 index 000000000000..a0f7f5b0980e --- /dev/null +++ b/data/ai/lua/ca_spread_poison.lua @@ -0,0 +1,100 @@ +------- Spread Poison CA -------------- + +local AH = wesnoth.require "ai/lua/ai_helper.lua" + +local ca_spread_poison = {} + +function ca_spread_poison:evaluation(cfg, data) + 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_live_units { side = wesnoth.current.side, + formula = 'attacks_left > 0', + { "filter_wml", { + { "attack", { + { "specials", { + { "poison", { } } + } } + } } + } }, + canrecruit = 'no' + } + 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) + 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 = - math.huge + 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 <= (attacker.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 defender:ability('regenerate') then rating = rating - 1000 end + + -- More priority to enemies on strong terrain + local defender_defense = 100 - defender:defense(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 - attacker:defense(wesnoth.get_terrain(a.dst.x, a.dst.y)) + rating = rating + attack_defense / 2. + + -- 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 best_attack then + 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 ca_spread_poison:execution(cfg, data) + local attacker = wesnoth.get_unit(data.attack.src.x, data.attack.src.y) + -- If several attacks have poison, this will always find the last one + local is_poisoner, poison_weapon = AH.has_weapon_special(attacker, "poison") + + if AH.print_exec() then AH.print_ts(' Executing spread_poison CA') end + if AH.show_messages() then wesnoth.wml_actions.message { speaker = attacker.id, message = 'Poison attack' } end + + AH.robust_move_and_attack(ai, attacker, data.attack.dst, data.attack.target, { weapon = poison_weapon }) + + data.attack = nil +end + +return ca_spread_poison diff --git a/data/ai/lua/ca_village_hunt.lua b/data/ai/lua/ca_village_hunt.lua new file mode 100644 index 000000000000..5eed2d60e3ad --- /dev/null +++ b/data/ai/lua/ca_village_hunt.lua @@ -0,0 +1,76 @@ +------- 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 + +local AH = wesnoth.require "ai/lua/ai_helper.lua" + +local ca_village_hunt = {} + +function ca_village_hunt:evaluation(cfg, data) + 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() + + 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 } + + if #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 AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 0 + end + + local units = wesnoth.get_units { + side = wesnoth.current.side, + canrecruit = false, + formula = 'movement_left > 0' + } + + if not units[1] then + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 0 + end + + if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end + return 30000 +end + +function ca_village_hunt:execution(cfg, data) + local unit = wesnoth.get_units({ + side = wesnoth.current.side, + canrecruit = false, + formula = 'movement_left > 0' + })[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 +end + +return ca_village_hunt diff --git a/data/ai/lua/generic_rush_engine.lua b/data/ai/lua/generic_rush_engine.lua deleted file mode 100644 index 71199ac4d204..000000000000 --- a/data/ai/lua/generic_rush_engine.lua +++ /dev/null @@ -1,612 +0,0 @@ -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 AH = wesnoth.require "ai/lua/ai_helper.lua" - local BC = wesnoth.require "ai/lua/battle_calcs.lua" - local HS = wesnoth.require "ai/micro_ais/cas/ca_healer_move.lua" - local R = wesnoth.require "ai/lua/retreat.lua" - local M = wesnoth.map - - ------ 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 - std_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(generic_rush, params) - - -------- Castle Switch CA -------------- - local function get_reachable_enemy_leaders(unit) - -- 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 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 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 leader = wesnoth.get_units { - side = wesnoth.current.side, - canrecruit = 'yes', - formula = '(movement_left = total_movement) and (hitpoints = 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 / 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 - 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 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 = 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 - 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 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 - 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 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, 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 AH.print_ts(' - 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 = 'movement_left > 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_attackable_enemies() - - 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 - - -- 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() - 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 = - math.huge - 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 - 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 = - math.huge - 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 AH.is_visible_unit(wesnoth.current.side, unit_in_way) and ((unit_in_way ~= u)) 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 = M.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 - 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 - 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 - - if best_village 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 AH.print_ts(' Executing grab_villages CA') end - if AH.show_messages() then wesnoth.wml_actions.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 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_live_units { side = wesnoth.current.side, - formula = 'attacks_left > 0', - { "filter_wml", { - { "attack", { - { "specials", { - { "poison", { } } - } } - } } - } }, - canrecruit = 'no' - } - 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) - 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 = - math.huge - 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 <= (attacker.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 defender:ability('regenerate') then rating = rating - 1000 end - - -- More priority to enemies on strong terrain - local defender_defense = 100 - defender:defense(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 - attacker:defense(wesnoth.get_terrain(a.dst.x, a.dst.y)) - rating = rating + attack_defense / 2. - - -- 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 best_attack 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 several attacks have poison, this will always find the last one - local is_poisoner, poison_weapon = AH.has_weapon_special(attacker, "poison") - - if AH.print_exec() then AH.print_ts(' Executing spread_poison CA') end - if AH.show_messages() then wesnoth.wml_actions.message { speaker = attacker.id, message = 'Poison attack' } end - - AH.robust_move_and_attack(ai, attacker, self.data.attack.dst, self.data.attack.target, { weapon = poison_weapon }) - - self.data.attack = nil - end - - ------- Place Healers CA -------------- - - function generic_rush:place_healers_eval() - local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'place_healers' - if AH.print_eval() then AH.print_ts(' - Evaluating place_healers CA:') end - - if HS:evaluation(ai, {}, self) > 0 then - if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 95000 - end - if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 0 - end - - function generic_rush:place_healers_exec() - if AH.print_exec() then AH.print_ts(' Executing place_healers CA') end - HS:execution(ai, nil, self) - end - - ------- Retreat CA -------------- - - function generic_rush:retreat_injured_units_eval() - local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'retreat_injured_units' - if AH.print_eval() then AH.print_ts(' - Evaluating retreat_injured_units CA:') end - - local units = wesnoth.get_units { - side = wesnoth.current.side, - formula = 'movement_left > 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 - if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 95000 - end - end - if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 205000 - end - if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 0 - end - - function generic_rush:retreat_injured_units_exec() - if AH.print_exec() then AH.print_ts(' Executing retreat_injured_units CA') end - AH.robust_move_and_attack(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 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() - - 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 } - - if #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 AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 0 - end - - local units = wesnoth.get_units { - side = wesnoth.current.side, - canrecruit = false, - formula = 'movement_left > 0' - } - - if not units[1] then - if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 0 - end - - if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 30000 - end - - function generic_rush:village_hunt_exec() - local unit = wesnoth.get_units({ - side = wesnoth.current.side, - canrecruit = false, - formula = 'movement_left > 0' - })[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 - end - - return generic_rush - end -} diff --git a/data/ai/lua/move_to_any_target.lua b/data/ai/lua/move_to_any_target.lua deleted file mode 100644 index 0e203137cc50..000000000000 --- a/data/ai/lua/move_to_any_target.lua +++ /dev/null @@ -1,59 +0,0 @@ -return { - init = function(ai) - local AH = wesnoth.require "ai/lua/ai_helper.lua" - - local move_to_any_target = {} - - function move_to_any_target:move_to_enemy_eval() - local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'move_to_enemy' - if AH.print_eval() then AH.print_ts(' - Evaluating move_to_enemy CA:') end - - local units = wesnoth.get_units { - side = wesnoth.current.side, - canrecruit = 'no', - formula = 'movement_left > 0' - } - - if (not units[1]) then - -- No units with moves left - if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 0 - end - - local unit, destination - -- Find a unit that has a path to an space close to an enemy - for i,u in ipairs(units) do - local distance, 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 then - break - 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 - - self.data.destination = destination - self.data.unit = unit - - if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end - return 1 - end - - function move_to_any_target:move_to_enemy_exec() - if AH.print_exec() then AH.print_ts(' Executing move_to_enemy CA') end - AH.checked_move(ai, self.data.unit, self.data.destination[1], self.data.destination[2]) - end - - return move_to_any_target - end -} diff --git a/data/core/macros/ai.cfg b/data/core/macros/ai.cfg index cc055a3d5b95..5c3cf509d89d 100644 --- a/data/core/macros/ai.cfg +++ b/data/core/macros/ai.cfg @@ -537,15 +537,6 @@ # move-to-enemy candidate actions. # Put this into the [side][ai] tag. # Does not work in [modify_side][ai] or [modify_ai] at the moment. - [engine] - name="lua" - code= << - local _,data = ... - local exp_ai = wesnoth.require("ai/lua/generic_rush_engine.lua").init(ai) - exp_ai.data = data - return exp_ai - >> - [/engine] [stage] id=main_loop name=ai_default_rca::candidate_action_evaluation_loop @@ -564,57 +555,49 @@ engine=lua name=recruit_rushers max_score=300000 - evaluation="return (...):recruit_rushers_eval()" - execution="(...):recruit_rushers_exec()" + location="ai/lua/ca_recruit_rushers.lua" [/candidate_action] [candidate_action] engine=lua name=switch_castle max_score=290000 - evaluation="return (...):castle_switch_eval()" - execution="(...):castle_switch_exec()" + location="ai/lua/ca_castle_switch.lua" [/candidate_action] [candidate_action] engine=lua name=retreat_injured max_score=205000 - evaluation="return (...):retreat_injured_units_eval()" - execution="(...):retreat_injured_units_exec()" + location="ai/lua/ca_retreat_injured.lua" [/candidate_action] [candidate_action] engine=lua name=grab_villages max_score=200000 - evaluation="return (...):grab_villages_eval()" - execution="(...):grab_villages_exec()" + location="ai/lua/ca_grab_villages.lua" [/candidate_action] [candidate_action] engine=lua name=spread_poison max_score=190000 - evaluation="return (...):spread_poison_eval()" - execution="(...):spread_poison_exec()" + location="ai/lua/ca_spread_poison.lua" [/candidate_action] [candidate_action] engine=lua name=place_healers max_score=95000 - evaluation="return (...):place_healers_eval()" - execution="(...):place_healers_exec()" + location="ai/lua/ca_place_healers.lua" [/candidate_action] [candidate_action] engine=lua - name=hunt_villages + name=village_hunt max_score=30000 - evaluation="return (...):village_hunt_eval()" - execution="(...):village_hunt_exec()" + location="ai/lua/ca_village_hunt.lua" [/candidate_action] [candidate_action] engine=lua - name=move_to_enemy + name=move_to_any_enemy max_score=1 - evaluation="return (...):move_to_enemy_eval()" - execution="(...):move_to_enemy_exec()" + location="ai/lua/ca_move_to_any_enemy.lua" [/candidate_action] [/stage] #enddef