diff --git a/changelog b/changelog index 457988fbe923..7114208e96d5 100644 --- a/changelog +++ b/changelog @@ -138,6 +138,9 @@ Version 1.13.4+dev: The table is read-only and raises an error if you attempt to write to it. * The way to create Lua candidate actions has changed a little. Old code will require minor changes. + * New wesnoth.micro_ais table contains the loaders for all Micro AIs. + New loaders can easily be installed by add-ons. See any built-in + micro AI (in ai/micro_ais/mai-defs/) for an example of how to do this. * Wesnoth formula engine: * Formulas in unit filters can now access nearly all unit attributes * New syntax features: diff --git a/data/ai/micro_ais/mai-defs/animals.lua b/data/ai/micro_ais/mai-defs/animals.lua new file mode 100644 index 000000000000..2f88270f40ef --- /dev/null +++ b/data/ai/micro_ais/mai-defs/animals.lua @@ -0,0 +1,133 @@ +local H = wesnoth.require "lua/helper.lua" +local MAIH = wesnoth.require("ai/micro_ais/micro_ai_helper.lua") + +function wesnoth.micro_ais.big_animals(cfg) + local required_keys = { "filter"} + local optional_keys = { "avoid_unit", "filter_location", "filter_location_wander" } + local CA_parms = { + ai_id = 'mai_big_animals', + { ca_id = "move", location = 'ca_big_animals.lua', score = cfg.ca_score or 300000 } + } + return required_keys, optional_keys, CA_parms +end + +function wesnoth.micro_ais.wolves(cfg) + local required_keys = { "filter", "filter_second" } + local optional_keys = { "attack_only_prey", "avoid_type" } + local score = cfg.ca_score or 90000 + local CA_parms = { + ai_id = 'mai_wolves', + { ca_id = "move", location = 'ca_wolves_move.lua', score = score }, + { ca_id = "wander", location = 'ca_wolves_wander.lua', score = score - 1 } + } + + if cfg.attack_only_prey then + local wolves_aspects = { + { + aspect = "attacks", + facet = { + name = "ai_default_rca::aspect_attacks", + id = "dont_attack", + invalidate_on_gamestate_change = "yes", + { "filter_enemy", { + { "and", H.get_child(cfg, "filter_second") } + } } + } + } + } + if (cfg.action == "delete") then + MAIH.delete_aspects(cfg.side, wolves_aspects) + else + MAIH.add_aspects(cfg.side, wolves_aspects) + end + elseif cfg.avoid_type then + local wolves_aspects = { + { + aspect = "attacks", + facet = { + name = "ai_default_rca::aspect_attacks", + id = "dont_attack", + invalidate_on_gamestate_change = "yes", + { "filter_enemy", { + { "not", { + type=cfg.avoid_type + } } + } } + } + } + } + if (cfg.action == "delete") then + MAIH.delete_aspects(cfg.side, wolves_aspects) + else + MAIH.add_aspects(cfg.side, wolves_aspects) + end + end + return required_keys, optional_keys, CA_parms +end + +function wesnoth.micro_ais.herding(cfg) + local required_keys = { "filter_location", "filter", "filter_second", "herd_x", "herd_y" } + local optional_keys = { "attention_distance", "attack_distance" } + local score = cfg.ca_score or 300000 + local CA_parms = { + ai_id = 'mai_herding', + { ca_id = "attack_close_enemy", location = 'ca_herding_attack_close_enemy.lua', score = score }, + { ca_id = "sheep_runs_enemy", location = 'ca_herding_sheep_runs_enemy.lua', score = score - 1 }, + { ca_id = "sheep_runs_dog", location = 'ca_herding_sheep_runs_dog.lua', score = score - 2 }, + { ca_id = "herd_sheep", location = 'ca_herding_herd_sheep.lua', score = score - 3 }, + { ca_id = "sheep_move", location = 'ca_herding_sheep_move.lua', score = score - 4 }, + { ca_id = "dog_move", location = 'ca_herding_dog_move.lua', score = score - 5 }, + { ca_id = "dog_stopmove", location = 'ca_herding_dog_stopmove.lua', score = score - 6 } + } + return required_keys, optional_keys, CA_parms +end + +function wesnoth.micro_ais.forest_animals(cfg) + local optional_keys = { "rabbit_type", "rabbit_number", "rabbit_enemy_distance", "rabbit_hole_img", + "tusker_type", "tusklet_type", "deer_type", "filter_location" + } + local score = cfg.ca_score or 300000 + local CA_parms = { + ai_id = 'mai_forest_animals', + { ca_id = "new_rabbit", location = 'ca_forest_animals_new_rabbit.lua', score = score }, + { ca_id = "tusker_attack", location = 'ca_forest_animals_tusker_attack.lua', score = score - 1 }, + { ca_id = "move", location = 'ca_forest_animals_move.lua', score = score - 2 }, + { ca_id = "tusklet_move", location = 'ca_forest_animals_tusklet_move.lua', score = score - 3 } + } + return {}, optional_keys, CA_parms +end + +function wesnoth.micro_ais.swarm(cfg) + local optional_keys = { "scatter_distance", "vision_distance", "enemy_distance" } + local score = cfg.ca_score or 300000 + local CA_parms = { + ai_id = 'mai_swarm', + { ca_id = "scatter", location = 'ca_swarm_scatter.lua', score = score }, + { ca_id = "move", location = 'ca_swarm_move.lua', score = score - 1 } + } + return {}, optional_keys, CA_parms +end + +function wesnoth.micro_ais.wolves_multipacks(cfg) + local optional_keys = { "type", "pack_size", "show_pack_number" } + local score = cfg.ca_score or 300000 + local CA_parms = { + ai_id = 'mai_wolves_multipacks', + { ca_id = "attack", location = 'ca_wolves_multipacks_attack.lua', score = score }, + { ca_id = "wander", location = 'ca_wolves_multipacks_wander.lua', score = score - 1 } + } + return {}, optional_keys, CA_parms +end + +function wesnoth.micro_ais.hunter(cfg) + if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then + H.wml_error("Hunter [micro_ai] tag requires either id= key or [filter] tag") + end + local required_keys = { "home_x", "home_y" } + local optional_keys = { "id", "filter", "filter_location", "rest_turns", "show_messages" } + local CA_parms = { + ai_id = 'mai_hunter', + { ca_id = "move", location = 'ca_hunter.lua', score = cfg.ca_score or 300000 } + } + return required_keys, optional_keys, CA_parms +end diff --git a/data/ai/micro_ais/mai-defs/bottleneck.lua b/data/ai/micro_ais/mai-defs/bottleneck.lua new file mode 100644 index 000000000000..b4da5b3b1942 --- /dev/null +++ b/data/ai/micro_ais/mai-defs/bottleneck.lua @@ -0,0 +1,12 @@ + +function wesnoth.micro_ais.bottleneck_defense(cfg) + local required_keys = { "x", "y", "enemy_x", "enemy_y" } + local optional_keys = { "healer_x", "healer_y", "leadership_x", "leadership_y", "active_side_leader" } + local score = cfg.ca_score or 300000 + local CA_parms = { + ai_id = 'mai_bottleneck', + { ca_id = 'move', location = 'ca_bottleneck_move.lua', score = score }, + { ca_id = 'attack', location = 'ca_bottleneck_attack.lua', score = score - 1 } + } + return required_keys, optional_keys, CA_parms +end \ No newline at end of file diff --git a/data/ai/micro_ais/mai-defs/escort.lua b/data/ai/micro_ais/mai-defs/escort.lua new file mode 100644 index 000000000000..8806686d6c79 --- /dev/null +++ b/data/ai/micro_ais/mai-defs/escort.lua @@ -0,0 +1,17 @@ +local H = wesnoth.require "lua/helper.lua" + +function wesnoth.micro_ais.messenger_escort(cfg) + if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then + H.wml_error("Messenger [micro_ai] tag requires either id= key or [filter] tag") + end + local required_keys = { "waypoint_x", "waypoint_y" } + local optional_keys = { "id", "enemy_death_chance", "filter", "filter_second", "invert_order", "messenger_death_chance" } + local score = cfg.ca_score or 300000 + local CA_parms = { + ai_id = 'mai_messenger', + { ca_id = 'attack', location = 'ca_messenger_attack.lua', score = score }, + { ca_id = 'move', location = 'ca_messenger_move.lua', score = score - 1 }, + { ca_id = 'escort_move', location = 'ca_messenger_escort_move.lua', score = score - 2 } + } + return required_keys, optional_keys, CA_parms +end diff --git a/data/ai/micro_ais/mai-defs/fast.lua b/data/ai/micro_ais/mai-defs/fast.lua new file mode 100644 index 000000000000..d65cda6f3ea1 --- /dev/null +++ b/data/ai/micro_ais/mai-defs/fast.lua @@ -0,0 +1,115 @@ +local H = wesnoth.require "lua/helper.lua" +local W = H.set_wml_action_metatable {} + +function wesnoth.micro_ais.fast_ai(cfg) + local optional_keys = { + "attack_hidden_enemies", "avoid", "dungeon_mode", + "filter", "filter_second", "include_occupied_attack_hexes", + "leader_additional_threat", "leader_attack_max_units", "leader_weight", "move_cost_factor", + "weak_units_first", "skip_combat_ca", "skip_move_ca", "threatened_leader_fights" + } + local CA_parms = { + ai_id = 'mai_fast', + { ca_id = 'combat', location = 'ca_fast_combat.lua', score = 100000 }, + { ca_id = 'move', location = 'ca_fast_move.lua', score = 20000 }, + { ca_id = 'combat_leader', location = 'ca_fast_combat_leader.lua', score = 19900 } + } + + -- Also need to delete/add some default CAs + if (cfg.action == 'delete') then + -- This can be done independently of whether these were removed earlier + W.modify_ai { + side = cfg.side, + action = "add", + path = "stage[main_loop].candidate_action", + { "candidate_action", { + id="combat", + engine="cpp", + name="ai_default_rca::combat_phase", + max_score=100000, + score=100000 + } } + } + + W.modify_ai { + side = cfg.side, + action = "add", + path = "stage[main_loop].candidate_action", + { "candidate_action", { + id="villages", + engine="cpp", + name="ai_default_rca::get_villages_phase", + max_score=60000, + score=60000 + } } + } + + W.modify_ai { + side = cfg.side, + action = "add", + path = "stage[main_loop].candidate_action", + { "candidate_action", { + id="retreat", + engine="cpp", + name="ai_default_rca::retreat_phase", + max_score=40000, + score=40000 + } } + } + + W.modify_ai { + side = cfg.side, + action = "add", + path = "stage[main_loop].candidate_action", + { "candidate_action", { + id="move_to_targets", + engine="cpp", + name="ai_default_rca::move_to_targets_phase", + max_score=20000, + score=20000 + } } + } + else + if (not cfg.skip_combat_ca) then + W.modify_ai { + side = cfg.side, + action = "try_delete", + path = "stage[main_loop].candidate_action[combat]" + } + else + for i,parm in ipairs(CA_parms) do + if (parm.ca_id == 'combat') or (parm.ca_id == 'combat_leader') then + table.remove(CA_parms, i) + end + end + end + + if (not cfg.skip_move_ca) then + W.modify_ai { + side = cfg.side, + action = "try_delete", + path = "stage[main_loop].candidate_action[villages]" + } + + W.modify_ai { + side = cfg.side, + action = "try_delete", + path = "stage[main_loop].candidate_action[retreat]" + } + + W.modify_ai { + side = cfg.side, + action = "try_delete", + path = "stage[main_loop].candidate_action[move_to_targets]" + } + else + for i,parm in ipairs(CA_parms) do + if (parm.ca_id == 'move') then + table.remove(CA_parms, i) + break + end + end + end + end + return {}, optional_keys, CA_parms +end diff --git a/data/ai/micro_ais/mai-defs/guardian.lua b/data/ai/micro_ais/mai-defs/guardian.lua new file mode 100644 index 000000000000..f600558dfc94 --- /dev/null +++ b/data/ai/micro_ais/mai-defs/guardian.lua @@ -0,0 +1,53 @@ +local H = wesnoth.require "lua/helper.lua" + +function wesnoth.micro_ais.stationed_guardian(cfg) + if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then + H.wml_error("Stationed Guardian [micro_ai] tag requires either id= key or [filter] tag") + end + local required_keys = { "distance", "station_x", "station_y" } + local optional_keys = { "id", "filter", "guard_x", "guard_y" } + local CA_parms = { + ai_id = 'mai_stationed_guardian', + { ca_id = 'move', location = 'ca_stationed_guardian.lua', score = cfg.ca_score or 300000 } + } + return required_keys, optional_keys, CA_parms +end + +function wesnoth.micro_ais.zone_guardian(cfg) + if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then + H.wml_error("Zone Guardian [micro_ai] tag requires either id= key or [filter] tag") + end + local required_keys = { "filter_location" } + local optional_keys = { "id", "filter", "filter_location_enemy", "station_x", "station_y" } + local CA_parms = { + ai_id = 'mai_zone_guardian', + { ca_id = 'move', location = 'ca_zone_guardian.lua', score = cfg.ca_score or 300000 } + } + return required_keys, optional_keys, CA_parms +end + +function wesnoth.micro_ais.return_guardian(cfg) + if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then + H.wml_error("Return Guardian [micro_ai] tag requires either id= key or [filter] tag") + end + local required_keys = { "return_x", "return_y" } + local optional_keys = { "id", "filter" } + local CA_parms = { + ai_id = 'mai_return_guardian', + { ca_id = 'move', location = 'ca_return_guardian.lua', score = cfg.ca_score or 100010 } + } + return required_keys, optional_keys, CA_parms +end + +function wesnoth.micro_ais.coward(cfg) + if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then + H.wml_error("Coward [micro_ai] tag requires either id= key or [filter] tag") + end + local required_keys = { "distance" } + local optional_keys = { "attack_if_trapped", "id", "filter", "filter_second", "seek_x", "seek_y","avoid_x","avoid_y" } + local CA_parms = { + ai_id = 'mai_coward', + { ca_id = 'move', location = 'ca_coward.lua', score = cfg.ca_score or 300000 } + } + return required_keys, optional_keys, CA_parms +end diff --git a/data/ai/micro_ais/mai-defs/healers.lua b/data/ai/micro_ais/mai-defs/healers.lua new file mode 100644 index 000000000000..4bd382b405c5 --- /dev/null +++ b/data/ai/micro_ais/mai-defs/healers.lua @@ -0,0 +1,17 @@ + +function wesnoth.micro_ais.healer_support(cfg) + local optional_keys = { "aggression", "injured_units_only", "max_threats", "filter", "filter_second" } + -- Scores for this AI need to be hard-coded, it does not work otherwise + local CA_parms = { + ai_id = 'mai_healer', + { ca_id = 'initialize', location = 'ca_healer_initialize.lua', score = 999990 }, + { ca_id = 'move', location = 'ca_healer_move.lua', score = 105000 }, + } + + -- The healers_can_attack CA is only added to the table if aggression ~= 0 + -- But: make sure we always try removal + if (cfg.action == 'delete') or (tonumber(cfg.aggression) ~= 0) then + table.insert(CA_parms, { ca_id = 'may_attack', location = 'ca_healer_may_attack.lua', score = 99990 }) + end + return {}, optional_keys, CA_parms +end \ No newline at end of file diff --git a/data/ai/micro_ais/mai-defs/misc.lua b/data/ai/micro_ais/mai-defs/misc.lua new file mode 100644 index 000000000000..2ed6ffd8b934 --- /dev/null +++ b/data/ai/micro_ais/mai-defs/misc.lua @@ -0,0 +1,42 @@ + +function wesnoth.micro_ais.lurkers(cfg) + local required_keys = { "filter", "filter_location" } + local optional_keys = { "stationary", "filter_location_wander" } + local CA_parms = { + ai_id = 'mai_lurkers', + { ca_id = 'move', location = 'ca_lurkers.lua', score = cfg.ca_score or 300000 } + } + return required_keys, optional_keys, CA_parms +end + +-- goto is a keyword, so need to use index operator directly +wesnoth.micro_ais["goto"] = function(cfg) + local required_keys = { "filter_location" } + local optional_keys = { + "avoid_enemies", "filter", "ignore_units", "ignore_enemy_at_goal", + "release_all_units_at_goal", "release_unit_at_goal", "unique_goals", "use_straight_line" + } + local CA_parms = { + ai_id = 'mai_goto', + { ca_id = 'move', location = 'ca_goto.lua', score = cfg.ca_score or 300000 } + } + return required_keys, optional_keys, CA_parms +end + +function wesnoth.micro_ais.hang_out(cfg) + local optional_keys = { "filter", "filter_location", "avoid", "mobilize_condition", "mobilize_on_gold_less_than" } + local CA_parms = { + ai_id = 'mai_hang_out', + { ca_id = 'move', location = 'ca_hang_out.lua', score = cfg.ca_score or 170000 } + } + return {}, optional_keys, CA_parms +end + +function wesnoth.micro_ais.simple_attack(cfg) + local optional_keys = { "filter", "filter_second", "weapon" } + local CA_parms = { + ai_id = 'mai_simple_attack', + { ca_id = 'move', location = 'ca_simple_attack.lua', score = cfg.ca_score or 110000 } + } + return {}, optional_keys, CA_parms +end diff --git a/data/ai/micro_ais/mai-defs/patrol.lua b/data/ai/micro_ais/mai-defs/patrol.lua new file mode 100644 index 000000000000..2c96e407673d --- /dev/null +++ b/data/ai/micro_ais/mai-defs/patrol.lua @@ -0,0 +1,14 @@ +local H = wesnoth.require "lua/helper.lua" + +function wesnoth.micro_ais.patrol(cfg) + if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then + H.wml_error("Patrol [micro_ai] tag requires either id= key or [filter] tag") + end + local required_keys = { "waypoint_x", "waypoint_y" } + local optional_keys = { "id", "filter", "attack", "one_time_only", "out_and_back" } + local CA_parms = { + ai_id = 'mai_patrol', + { ca_id = "move", location = 'ca_patrol.lua', score = cfg.ca_score or 300000 } + } + return required_keys, optional_keys, CA_parms +end diff --git a/data/ai/micro_ais/mai-defs/protect.lua b/data/ai/micro_ais/mai-defs/protect.lua new file mode 100644 index 000000000000..3c73d540e051 --- /dev/null +++ b/data/ai/micro_ais/mai-defs/protect.lua @@ -0,0 +1,89 @@ +local H = wesnoth.require "lua/helper.lua" +local W = H.set_wml_action_metatable {} +local MAIH = wesnoth.require("ai/micro_ais/micro_ai_helper.lua") + +function wesnoth.micro_ais.protect_unit(cfg) + local required_keys = { "id", "goal_x", "goal_y" } + -- Scores for this AI need to be hard-coded, it does not work otherwise + local CA_parms = { + ai_id = 'mai_protect_unit', + { ca_id = 'finish', location = 'ca_protect_unit_finish.lua', score = 300000 }, + { ca_id = 'attack', location = 'ca_protect_unit_attack.lua', score = 95000 }, + { ca_id = 'move', location = 'ca_protect_unit_move.lua', score = 94999 } + } + + -- [unit] tags need to be dealt with separately + cfg.id, cfg.goal_x, cfg.goal_y = {}, {}, {} + if (cfg.action ~= 'delete') then + for unit in H.child_range(cfg, "unit") do + if (not unit.id) then + H.wml_error("Protect Unit Micro AI [unit] tag is missing required id= key") + end + if (not unit.goal_x) then + H.wml_error("Protect Unit Micro AI [unit] tag is missing required goal_x= key") + end + if (not unit.goal_y) then + H.wml_error("Protect Unit Micro AI [unit] tag is missing required goal_y= key") + end + table.insert(cfg.id, unit.id) + table.insert(cfg.goal_x, unit.goal_x) + table.insert(cfg.goal_y, unit.goal_y) + end + + if (not cfg.id[1]) then + H.wml_error("Protect Unit Micro AI is missing required [unit] tag") + end + end + + -- Optional key disable_move_leader_to_keep: needs to be dealt with + -- separately as it affects a default CA + if cfg.disable_move_leader_to_keep then + W.modify_ai { + side = cfg.side, + action = "try_delete", + path = "stage[main_loop].candidate_action[move_leader_to_keep]" + } + end + + -- attacks aspects also needs to be set separately + local unit_ids_str = 'dummy' + for _,id in ipairs(cfg.id) do + unit_ids_str = unit_ids_str .. ',' .. id + end + local aspect_parms = { + { + aspect = "attacks", + facet = { + name = "ai_default_rca::aspect_attacks", + ca_id = "dont_attack", + invalidate_on_gamestate_change = "yes", + { "filter_own", { + { "not", { + id = unit_ids_str + } } + } } + } + } + } + + if (cfg.action == "delete") then + MAIH.delete_aspects(cfg.side, aspect_parms) + -- We also need to add the move_leader_to_keep CA back in + -- This works even if it was not removed, it simply overwrites the existing CA + W.modify_ai { + side = side, + action = "add", + path = "stage[main_loop].candidate_action", + { "candidate_action", { + id="move_leader_to_keep", + engine="cpp", + name="ai_default_rca::move_leader_to_keep_phase", + max_score=160000, + score=160000 + } } + } + else + MAIH.add_aspects(cfg.side, aspect_parms) + end + return required_keys, {}, CA_parms +end diff --git a/data/ai/micro_ais/mai-defs/recruiting.lua b/data/ai/micro_ais/mai-defs/recruiting.lua new file mode 100644 index 000000000000..3d02a9293d48 --- /dev/null +++ b/data/ai/micro_ais/mai-defs/recruiting.lua @@ -0,0 +1,65 @@ +local H = wesnoth.require "lua/helper.lua" +local W = H.set_wml_action_metatable {} + +local function handle_default_recruitment(cfg) + -- Also need to delete/add the default recruitment CA + if cfg.action == 'add' then + W.modify_ai { + side = cfg.side, + action = "try_delete", + path = "stage[main_loop].candidate_action[recruitment]" + } + elseif cfg.action == 'delete' then + -- We need to add the recruitment CA back in + -- This works even if it was not removed, it simply overwrites the existing CA + W.modify_ai { + side = cfg.side, + action = "add", + path = "stage[main_loop].candidate_action", + { "candidate_action", { + id="recruitment", + engine="cpp", + name="ai_default_rca::aspect_recruitment_phase", + max_score=180000, + score=180000 + } } + } + end +end + +function wesnoth.micro_ais.recruit_rushers(cfg) + local optional_keys = { "randomness" } + local CA_parms = { + ai_id = 'mai_rusher_recruit', + { ca_id = "move", location = 'ca_recruit_rushers.lua', score = cfg.ca_score or 180000 } + } + + handle_default_recruitment(cfg) + return {}, optional_keys, CA_parms +end + +function wesnoth.micro_ais.recruit_random(cfg) + local optional_keys = { "skip_low_gold_recruiting", "type", "prob" } + local CA_parms = { + ai_id = 'mai_random_recruit', + { ca_id = "move", location = 'ca_recruit_random.lua', score = cfg.ca_score or 180000 } + } + + if (cfg.action ~= 'delete') then + -- The 'probability' tags need to be handled separately here + cfg.type, cfg.prob = {}, {} + for probability in H.child_range(cfg, "probability") do + if (not probability.type) then + H.wml_error("Random Recruiting Micro AI [probability] tag is missing required type= key") + end + if (not probability.probability) then + H.wml_error("Random Recruiting Micro AI [probability] tag is missing required probability= key") + end + table.insert(cfg.type, probability.type) + table.insert(cfg.prob, probability.probability) + end + end + + handle_default_recruitment(cfg) + return {}, optional_keys, CA_parms +end \ No newline at end of file diff --git a/data/ai/micro_ais/micro_ai_wml_tag.lua b/data/ai/micro_ais/micro_ai_wml_tag.lua index 2f21d17945d8..9c92218624a6 100644 --- a/data/ai/micro_ais/micro_ai_wml_tag.lua +++ b/data/ai/micro_ais/micro_ai_wml_tag.lua @@ -2,6 +2,20 @@ local H = wesnoth.require "lua/helper.lua" local W = H.set_wml_action_metatable {} local MAIH = wesnoth.require("ai/micro_ais/micro_ai_helper.lua") +wesnoth.micro_ais = {} + +-- Load all default MicroAIs +wesnoth.require("ai/micro_ais/mai-defs/animals.lua") +wesnoth.require("ai/micro_ais/mai-defs/bottleneck.lua") +wesnoth.require("ai/micro_ais/mai-defs/escort.lua") +wesnoth.require("ai/micro_ais/mai-defs/fast.lua") +wesnoth.require("ai/micro_ais/mai-defs/guardian.lua") +wesnoth.require("ai/micro_ais/mai-defs/healers.lua") +wesnoth.require("ai/micro_ais/mai-defs/misc.lua") +wesnoth.require("ai/micro_ais/mai-defs/patrol.lua") +wesnoth.require("ai/micro_ais/mai-defs/protect.lua") +wesnoth.require("ai/micro_ais/mai-defs/recruiting.lua") + function wesnoth.wml_actions.micro_ai(cfg) local CA_path = 'ai/micro_ais/cas/' @@ -21,518 +35,18 @@ function wesnoth.wml_actions.micro_ai(cfg) end -- Set up the configuration tables for the different Micro AIs - local required_keys, optional_keys, CA_parms = {}, {}, {} - - --------- Healer Support Micro AI ------------------------------------ - if (cfg.ai_type == 'healer_support') then - optional_keys = { "aggression", "injured_units_only", "max_threats", "filter", "filter_second" } - -- Scores for this AI need to be hard-coded, it does not work otherwise - CA_parms = { - ai_id = 'mai_healer', - { ca_id = 'initialize', location = CA_path .. 'ca_healer_initialize.lua', score = 999990 }, - { ca_id = 'move', location = CA_path .. 'ca_healer_move.lua', score = 105000 }, - } - - -- The healers_can_attack CA is only added to the table if aggression ~= 0 - -- But: make sure we always try removal - if (cfg.action == 'delete') or (tonumber(cfg.aggression) ~= 0) then - table.insert(CA_parms, { ca_id = 'may_attack', location = CA_path .. 'ca_healer_may_attack.lua', score = 99990 }) - end - - --------- Bottleneck Defense Micro AI ----------------------------------- - elseif (cfg.ai_type == 'bottleneck_defense') then - required_keys = { "x", "y", "enemy_x", "enemy_y" } - optional_keys = { "healer_x", "healer_y", "leadership_x", "leadership_y", "active_side_leader" } - local score = cfg.ca_score or 300000 - CA_parms = { - ai_id = 'mai_bottleneck', - { ca_id = 'move', location = CA_path .. 'ca_bottleneck_move.lua', score = score }, - { ca_id = 'attack', location = CA_path .. 'ca_bottleneck_attack.lua', score = score - 1 } - } - - --------- Messenger Escort Micro AI ------------------------------------ - elseif (cfg.ai_type == 'messenger_escort') then - if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then - H.wml_error("Messenger [micro_ai] tag requires either id= key or [filter] tag") - end - required_keys = { "waypoint_x", "waypoint_y" } - optional_keys = { "id", "enemy_death_chance", "filter", "filter_second", "invert_order", "messenger_death_chance" } - local score = cfg.ca_score or 300000 - CA_parms = { - ai_id = 'mai_messenger', - { ca_id = 'attack', location = CA_path .. 'ca_messenger_attack.lua', score = score }, - { ca_id = 'move', location = CA_path .. 'ca_messenger_move.lua', score = score - 1 }, - { ca_id = 'escort_move', location = CA_path .. 'ca_messenger_escort_move.lua', score = score - 2 } - } - - --------- Lurkers Micro AI ------------------------------------ - elseif (cfg.ai_type == 'lurkers') then - required_keys = { "filter", "filter_location" } - optional_keys = { "stationary", "filter_location_wander" } - CA_parms = { - ai_id = 'mai_lurkers', - { ca_id = 'move', location = CA_path .. 'ca_lurkers.lua', score = cfg.ca_score or 300000 } - } - - --------- Protect Unit Micro AI ------------------------------------ - elseif (cfg.ai_type == 'protect_unit') then - required_keys = { "id", "goal_x", "goal_y" } - -- Scores for this AI need to be hard-coded, it does not work otherwise - CA_parms = { - ai_id = 'mai_protect_unit', - { ca_id = 'finish', location = CA_path .. 'ca_protect_unit_finish.lua', score = 300000 }, - { ca_id = 'attack', location = CA_path .. 'ca_protect_unit_attack.lua', score = 95000 }, - { ca_id = 'move', location = CA_path .. 'ca_protect_unit_move.lua', score = 94999 } - } - - -- [unit] tags need to be dealt with separately - cfg.id, cfg.goal_x, cfg.goal_y = {}, {}, {} - if (cfg.action ~= 'delete') then - for unit in H.child_range(cfg, "unit") do - if (not unit.id) then - H.wml_error("Protect Unit Micro AI [unit] tag is missing required id= key") - end - if (not unit.goal_x) then - H.wml_error("Protect Unit Micro AI [unit] tag is missing required goal_x= key") - end - if (not unit.goal_y) then - H.wml_error("Protect Unit Micro AI [unit] tag is missing required goal_y= key") - end - table.insert(cfg.id, unit.id) - table.insert(cfg.goal_x, unit.goal_x) - table.insert(cfg.goal_y, unit.goal_y) - end - - if (not cfg.id[1]) then - H.wml_error("Protect Unit Micro AI is missing required [unit] tag") - end - end - - -- Optional key disable_move_leader_to_keep: needs to be dealt with - -- separately as it affects a default CA - if cfg.disable_move_leader_to_keep then - W.modify_ai { - side = cfg.side, - action = "try_delete", - path = "stage[main_loop].candidate_action[move_leader_to_keep]" - } - end - - -- attacks aspects also needs to be set separately - local unit_ids_str = 'dummy' - for _,id in ipairs(cfg.id) do - unit_ids_str = unit_ids_str .. ',' .. id - end - local aspect_parms = { - { - aspect = "attacks", - facet = { - name = "ai_default_rca::aspect_attacks", - ca_id = "dont_attack", - invalidate_on_gamestate_change = "yes", - { "filter_own", { - { "not", { - id = unit_ids_str - } } - } } - } - } - } - - if (cfg.action == "delete") then - MAIH.delete_aspects(cfg.side, aspect_parms) - -- We also need to add the move_leader_to_keep CA back in - -- This works even if it was not removed, it simply overwrites the existing CA - W.modify_ai { - side = side, - action = "add", - path = "stage[main_loop].candidate_action", - { "candidate_action", { - id="move_leader_to_keep", - engine="cpp", - name="ai_default_rca::move_leader_to_keep_phase", - max_score=160000, - score=160000 - } } - } - else - MAIH.add_aspects(cfg.side, aspect_parms) - end - - --------- Micro AI Guardian ----------------------------------- - elseif (cfg.ai_type == 'stationed_guardian') then - if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then - H.wml_error("Stationed Guardian [micro_ai] tag requires either id= key or [filter] tag") - end - required_keys = { "distance", "station_x", "station_y" } - optional_keys = { "id", "filter", "guard_x", "guard_y" } - CA_parms = { - ai_id = 'mai_stationed_guardian', - { ca_id = 'move', location = CA_path .. 'ca_stationed_guardian.lua', score = cfg.ca_score or 300000 } - } - - elseif (cfg.ai_type == 'zone_guardian') then - if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then - H.wml_error("Zone Guardian [micro_ai] tag requires either id= key or [filter] tag") - end - required_keys = { "filter_location" } - optional_keys = { "id", "filter", "filter_location_enemy", "station_x", "station_y" } - CA_parms = { - ai_id = 'mai_zone_guardian', - { ca_id = 'move', location = CA_path .. 'ca_zone_guardian.lua', score = cfg.ca_score or 300000 } - } - - elseif (cfg.ai_type == 'return_guardian') then - if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then - H.wml_error("Return Guardian [micro_ai] tag requires either id= key or [filter] tag") - end - required_keys = { "return_x", "return_y" } - optional_keys = { "id", "filter" } - CA_parms = { - ai_id = 'mai_return_guardian', - { ca_id = 'move', location = CA_path .. 'ca_return_guardian.lua', score = cfg.ca_score or 100010 } - } - - elseif (cfg.ai_type == 'coward') then - if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then - H.wml_error("Coward [micro_ai] tag requires either id= key or [filter] tag") - end - required_keys = { "distance" } - optional_keys = { "attack_if_trapped", "id", "filter", "filter_second", "seek_x", "seek_y","avoid_x","avoid_y" } - CA_parms = { - ai_id = 'mai_coward', - { ca_id = 'move', location = CA_path .. 'ca_coward.lua', score = cfg.ca_score or 300000 } - } - - --------- Micro AI Animals ------------------------------------ - elseif (cfg.ai_type == 'big_animals') then - required_keys = { "filter"} - optional_keys = { "avoid_unit", "filter_location", "filter_location_wander" } - CA_parms = { - ai_id = 'mai_big_animals', - { ca_id = "move", location = CA_path .. 'ca_big_animals.lua', score = cfg.ca_score or 300000 } - } - - elseif (cfg.ai_type == 'wolves') then - required_keys = { "filter", "filter_second" } - optional_keys = { "attack_only_prey", "avoid_type" } - local score = cfg.ca_score or 90000 - CA_parms = { - ai_id = 'mai_wolves', - { ca_id = "move", location = CA_path .. 'ca_wolves_move.lua', score = score }, - { ca_id = "wander", location = CA_path .. 'ca_wolves_wander.lua', score = score - 1 } - } - - if cfg.attack_only_prey then - local wolves_aspects = { - { - aspect = "attacks", - facet = { - name = "ai_default_rca::aspect_attacks", - id = "dont_attack", - invalidate_on_gamestate_change = "yes", - { "filter_enemy", { - { "and", H.get_child(cfg, "filter_second") } - } } - } - } - } - if (cfg.action == "delete") then - MAIH.delete_aspects(cfg.side, wolves_aspects) - else - MAIH.add_aspects(cfg.side, wolves_aspects) - end - elseif cfg.avoid_type then - local wolves_aspects = { - { - aspect = "attacks", - facet = { - name = "ai_default_rca::aspect_attacks", - id = "dont_attack", - invalidate_on_gamestate_change = "yes", - { "filter_enemy", { - { "not", { - type=cfg.avoid_type - } } - } } - } - } - } - if (cfg.action == "delete") then - MAIH.delete_aspects(cfg.side, wolves_aspects) - else - MAIH.add_aspects(cfg.side, wolves_aspects) - end - end - - elseif (cfg.ai_type == 'herding') then - required_keys = { "filter_location", "filter", "filter_second", "herd_x", "herd_y" } - optional_keys = { "attention_distance", "attack_distance" } - local score = cfg.ca_score or 300000 - CA_parms = { - ai_id = 'mai_herding', - { ca_id = "attack_close_enemy", location = CA_path .. 'ca_herding_attack_close_enemy.lua', score = score }, - { ca_id = "sheep_runs_enemy", location = CA_path .. 'ca_herding_sheep_runs_enemy.lua', score = score - 1 }, - { ca_id = "sheep_runs_dog", location = CA_path .. 'ca_herding_sheep_runs_dog.lua', score = score - 2 }, - { ca_id = "herd_sheep", location = CA_path .. 'ca_herding_herd_sheep.lua', score = score - 3 }, - { ca_id = "sheep_move", location = CA_path .. 'ca_herding_sheep_move.lua', score = score - 4 }, - { ca_id = "dog_move", location = CA_path .. 'ca_herding_dog_move.lua', score = score - 5 }, - { ca_id = "dog_stopmove", location = CA_path .. 'ca_herding_dog_stopmove.lua', score = score - 6 } - } - - elseif (cfg.ai_type == 'forest_animals') then - optional_keys = { "rabbit_type", "rabbit_number", "rabbit_enemy_distance", "rabbit_hole_img", - "tusker_type", "tusklet_type", "deer_type", "filter_location" - } - local score = cfg.ca_score or 300000 - CA_parms = { - ai_id = 'mai_forest_animals', - { ca_id = "new_rabbit", location = CA_path .. 'ca_forest_animals_new_rabbit.lua', score = score }, - { ca_id = "tusker_attack", location = CA_path .. 'ca_forest_animals_tusker_attack.lua', score = score - 1 }, - { ca_id = "move", location = CA_path .. 'ca_forest_animals_move.lua', score = score - 2 }, - { ca_id = "tusklet_move", location = CA_path .. 'ca_forest_animals_tusklet_move.lua', score = score - 3 } - } - - elseif (cfg.ai_type == 'swarm') then - optional_keys = { "scatter_distance", "vision_distance", "enemy_distance" } - local score = cfg.ca_score or 300000 - CA_parms = { - ai_id = 'mai_swarm', - { ca_id = "scatter", location = CA_path .. 'ca_swarm_scatter.lua', score = score }, - { ca_id = "move", location = CA_path .. 'ca_swarm_move.lua', score = score - 1 } - } - - elseif (cfg.ai_type == 'wolves_multipacks') then - optional_keys = { "type", "pack_size", "show_pack_number" } - local score = cfg.ca_score or 300000 - CA_parms = { - ai_id = 'mai_wolves_multipacks', - { ca_id = "attack", location = CA_path .. 'ca_wolves_multipacks_attack.lua', score = score }, - { ca_id = "wander", location = CA_path .. 'ca_wolves_multipacks_wander.lua', score = score - 1 } - } - - elseif (cfg.ai_type == 'hunter') then - if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then - H.wml_error("Hunter [micro_ai] tag requires either id= key or [filter] tag") - end - required_keys = { "home_x", "home_y" } - optional_keys = { "id", "filter", "filter_location", "rest_turns", "show_messages" } - CA_parms = { - ai_id = 'mai_hunter', - { ca_id = "move", location = CA_path .. 'ca_hunter.lua', score = cfg.ca_score or 300000 } - } - - --------- Patrol Micro AI ------------------------------------ - elseif (cfg.ai_type == 'patrol') then - if (cfg.action ~= 'delete') and (not cfg.id) and (not H.get_child(cfg, "filter")) then - H.wml_error("Patrol [micro_ai] tag requires either id= key or [filter] tag") - end - required_keys = { "waypoint_x", "waypoint_y" } - optional_keys = { "id", "filter", "attack", "one_time_only", "out_and_back" } - CA_parms = { - ai_id = 'mai_patrol', - { ca_id = "move", location = CA_path .. 'ca_patrol.lua', score = cfg.ca_score or 300000 } - } - - --------- Recruiting Micro AI ------------------------------------ - elseif (cfg.ai_type == 'recruit_rushers') or (cfg.ai_type == 'recruit_random')then - if (cfg.ai_type == 'recruit_rushers') then - optional_keys = { "randomness" } - CA_parms = { - ai_id = 'mai_rusher_recruit', - { ca_id = "move", location = CA_path .. 'ca_recruit_rushers.lua', score = cfg.ca_score or 180000 } - } - - else - optional_keys = { "skip_low_gold_recruiting", "type", "prob" } - CA_parms = { - ai_id = 'mai_random_recruit', - { ca_id = "move", location = CA_path .. 'ca_recruit_random.lua', score = cfg.ca_score or 180000 } - } - - if (cfg.action ~= 'delete') then - -- The 'probability' tags need to be handled separately here - cfg.type, cfg.prob = {}, {} - for probability in H.child_range(cfg, "probability") do - if (not probability.type) then - H.wml_error("Random Recruiting Micro AI [probability] tag is missing required type= key") - end - if (not probability.probability) then - H.wml_error("Random Recruiting Micro AI [probability] tag is missing required probability= key") - end - table.insert(cfg.type, probability.type) - table.insert(cfg.prob, probability.probability) - end - end - end - - -- Also need to delete/add the default recruitment CA - if cfg.action == 'add' then - W.modify_ai { - side = cfg.side, - action = "try_delete", - path = "stage[main_loop].candidate_action[recruitment]" - } - elseif cfg.action == 'delete' then - -- We need to add the recruitment CA back in - -- This works even if it was not removed, it simply overwrites the existing CA - W.modify_ai { - side = cfg.side, - action = "add", - path = "stage[main_loop].candidate_action", - { "candidate_action", { - id="recruitment", - engine="cpp", - name="ai_default_rca::aspect_recruitment_phase", - max_score=180000, - score=180000 - } } - } - end - - --------- Goto Micro AI ------------------------------------ - elseif (cfg.ai_type == 'goto') then - required_keys = { "filter_location" } - optional_keys = { - "avoid_enemies", "filter", "ignore_units", "ignore_enemy_at_goal", - "release_all_units_at_goal", "release_unit_at_goal", "unique_goals", "use_straight_line" - } - CA_parms = { - ai_id = 'mai_goto', - { ca_id = 'move', location = CA_path .. 'ca_goto.lua', score = cfg.ca_score or 300000 } - } - - --------- Hang Out Micro AI ------------------------------------ - elseif (cfg.ai_type == 'hang_out') then - optional_keys = { "filter", "filter_location", "avoid", "mobilize_condition", "mobilize_on_gold_less_than" } - CA_parms = { - ai_id = 'mai_hang_out', - { ca_id = 'move', location = CA_path .. 'ca_hang_out.lua', score = cfg.ca_score or 170000 } - } - - --------- Simple Attack Micro AI --------------------------- - elseif (cfg.ai_type == 'simple_attack') then - optional_keys = { "filter", "filter_second", "weapon" } - CA_parms = { - ai_id = 'mai_simple_attack', - { ca_id = 'move', location = CA_path .. 'ca_simple_attack.lua', score = cfg.ca_score or 110000 } - } - - elseif (cfg.ai_type == 'fast_ai') then - optional_keys = { - "attack_hidden_enemies", "avoid", "dungeon_mode", - "filter", "filter_second", "include_occupied_attack_hexes", - "leader_additional_threat", "leader_attack_max_units", "leader_weight", "move_cost_factor", - "weak_units_first", "skip_combat_ca", "skip_move_ca", "threatened_leader_fights" - } - CA_parms = { - ai_id = 'mai_fast', - { ca_id = 'combat', location = CA_path .. 'ca_fast_combat.lua', score = 100000 }, - { ca_id = 'move', location = CA_path .. 'ca_fast_move.lua', score = 20000 }, - { ca_id = 'combat_leader', location = CA_path .. 'ca_fast_combat_leader.lua', score = 19900 } - } - - -- Also need to delete/add some default CAs - if (cfg.action == 'delete') then - -- This can be done independently of whether these were removed earlier - W.modify_ai { - side = cfg.side, - action = "add", - path = "stage[main_loop].candidate_action", - { "candidate_action", { - id="combat", - engine="cpp", - name="ai_default_rca::combat_phase", - max_score=100000, - score=100000 - } } - } - - W.modify_ai { - side = cfg.side, - action = "add", - path = "stage[main_loop].candidate_action", - { "candidate_action", { - id="villages", - engine="cpp", - name="ai_default_rca::get_villages_phase", - max_score=60000, - score=60000 - } } - } - - W.modify_ai { - side = cfg.side, - action = "add", - path = "stage[main_loop].candidate_action", - { "candidate_action", { - id="retreat", - engine="cpp", - name="ai_default_rca::retreat_phase", - max_score=40000, - score=40000 - } } - } - - W.modify_ai { - side = cfg.side, - action = "add", - path = "stage[main_loop].candidate_action", - { "candidate_action", { - id="move_to_targets", - engine="cpp", - name="ai_default_rca::move_to_targets_phase", - max_score=20000, - score=20000 - } } - } - else - if (not cfg.skip_combat_ca) then - W.modify_ai { - side = cfg.side, - action = "try_delete", - path = "stage[main_loop].candidate_action[combat]" - } - else - for i,parm in ipairs(CA_parms) do - if (parm.ca_id == 'combat') or (parm.ca_id == 'combat_leader') then - table.remove(CA_parms, i) - end - end - end - - if (not cfg.skip_move_ca) then - W.modify_ai { - side = cfg.side, - action = "try_delete", - path = "stage[main_loop].candidate_action[villages]" - } - - W.modify_ai { - side = cfg.side, - action = "try_delete", - path = "stage[main_loop].candidate_action[retreat]" - } - - W.modify_ai { - side = cfg.side, - action = "try_delete", - path = "stage[main_loop].candidate_action[move_to_targets]" - } - else - for i,parm in ipairs(CA_parms) do - if (parm.ca_id == 'move') then - table.remove(CA_parms, i) - break - end - end - end - end - - -- If we got here, none of the valid ai_types was specified - else + if wesnoth.micro_ais[cfg.ai_type] == nil then H.wml_error("unknown value for ai_type= in [micro_ai]") end - + + local required_keys, optional_keys, CA_parms = wesnoth.micro_ais[cfg.ai_type](cfg) + + -- Fixup any relative CA paths + for i,v in ipairs(CA_parms) do + if v.location and v.location:find('~') ~= 1 then + v.location = CA_path .. v.location + end + end + MAIH.micro_ai_setup(cfg, CA_parms, required_keys, optional_keys) end