MicroAIs: Refactor code to make it easy for add-ons to add new ones

This commit is contained in:
Celtic Minstrel 2016-03-26 21:56:59 -04:00
parent 6ed34069fe
commit c19ddba30a
12 changed files with 585 additions and 511 deletions

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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