Merge pull request #5630 from mattsc/remove_fai_uses

Remove Formula AI uses
This commit is contained in:
mattsc 2021-03-24 08:04:47 -07:00 committed by GitHub
commit f1c37f64ac
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 191 additions and 276 deletions

View file

@ -1,33 +0,0 @@
#textdomain wesnoth-lib
#ifndef AI_CA_GOTO
{core/macros/ai.cfg}
{core/macros/ai_candidate_actions.cfg}
#endif
[ai]
id=formula_ai # id is needed to uniquely identify a MP AI, it is not needed in the scenario AI
description=_"Multiplayer_AI^Dev AI: Default + Experimental Recruitment (Formula AI)" # wmllint: no spellcheck
mp_rank=100000
# this description is, again, needed for MP AI (it shows in AI list under this description
[stage]
engine=fai
name=side_formulas
move="{ai/formula/new_recruitment.fai}"
[/stage]
[stage]
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
{AI_CA_GOTO}
#{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_HIGH_XP_ATTACK}
{AI_CA_COMBAT}
{AI_CA_HEALING}
{AI_CA_VILLAGES}
{AI_CA_RETREAT}
{AI_CA_MOVE_TO_TARGETS}
[/stage]
[/ai]

View file

@ -1,37 +0,0 @@
#textdomain wesnoth-lib
#ifndef AI_CA_GOTO
{core/macros/ai_candidate_actions.cfg}
#endif
[ai]
id=formula_ai_poisoning
description=_"Multiplayer_AI^Dev AI: Default + Poisoning (Formula AI)" # wmllint: no spellcheck
mp_rank=100000
[stage]
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
{AI_CA_GOTO}
{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_HIGH_XP_ATTACK}
{AI_CA_COMBAT}
{AI_CA_HEALING}
{AI_CA_VILLAGES}
{AI_CA_RETREAT}
{AI_CA_MOVE_TO_TARGETS}
{AI_CA_LEADER_SHARES_KEEP}
[candidate_action] #it consists of several candidate actions
engine=fai # fai engine is temporary disabled, it will be enabled real soon. Note that the rest of parameters are parsed by formula ai engine
name=poisoner #this paramerer (and the rest of them), are specific to fai engine
type=attack
[filter]
me="filter( input, 'me', filter(me.attacks,'att',filter(att.special,'spe',contains_string(spe,'poison'))))"
target="filter( input, 'target', target.undead = 0 and target.hitpoints > 5 and index_of('poisoned',keys(target.states)) = -1)"
[/filter]
evaluation="{ai/formula/poisoner_eval.fai}"
action="{ai/formula/poisoner_attack.fai}"
[/candidate_action]
[/stage]
[/ai]

View file

@ -1,4 +1,5 @@
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local BC = wesnoth.dofile "ai/lua/battle_calcs.lua"
local MAIUV = wesnoth.require "ai/micro_ais/micro_ai_unit_variables.lua"
local function get_patrol(cfg)
@ -10,6 +11,78 @@ local function get_patrol(cfg)
return patrol
end
local function get_best_attack(unit, loc, last_waypoint, cfg)
local attack_range = cfg.attack_range or 1
-- The attack calculation can be somewhat expensive, check first if there are enemies within the specified range
local enemies = AH.get_attackable_enemies(
{ id = cfg.attack, { "filter_location", { x = loc[1], y = loc[2], radius = attack_range } } },
wesnoth.current.side,
{ ignore_visibility = cfg.attack_invisible_enemies }
)
-- An enemy on the last waypoint gets attacked preferentially and independent of
-- whether its id is given in cfg.attack; but it still needs to be within attack range
local enemy_last_waypoint
if last_waypoint then
enemy_last_waypoint = AH.get_attackable_enemies(
{ x = last_waypoint[1], y = last_waypoint[2] },
wesnoth.current.side,
{ ignore_visibility = cfg.attack_invisible_enemies }
)[1]
if enemy_last_waypoint and (wesnoth.map.distance_between(enemy_last_waypoint, loc) <= attack_range) then
local already_included = false
for _,enemy in ipairs(enemies) do
if (enemy.id == enemy_last_waypoint.id) then
already_included = true
break
end
end
if (not already_included) then
table.insert(enemies, enemy_last_waypoint)
end
end
end
if (#enemies == 0) then return end
local old_moves, old_loc
if ((loc[1] ~= unit.x) or (loc[2] ~= unit.y)) then
old_moves, old_loc = unit.moves, unit.loc
local _,sub_cost = AH.find_path_with_shroud(unit, loc)
unit.moves = unit.moves - sub_cost
unit.loc = loc
end
local attacks = AH.get_attacks({ unit }, { ignore_visibility = cfg.attack_invisible_enemies })
if old_moves then
unit.moves, unit.loc = old_moves, old_loc
end
local max_rating, best_enemy, best_dst = -math.huge
for _,attack in ipairs(attacks) do
for _,enemy in ipairs(enemies) do
if (attack.target.x == enemy.x) and (attack.target.y == enemy.y) then
local dst = { attack.dst.x, attack.dst.y }
local rating = BC.attack_rating(unit, enemy, dst)
-- Prioritize any enemy on the last waypoint
if enemy_last_waypoint and (enemy_last_waypoint.id == enemy.id) then
rating = rating + 1000
end
if (rating > max_rating) then
max_rating = rating
best_enemy = enemy
best_dst = dst
end
break
end
end
end
return best_enemy, best_dst
end
local ca_patrol = {}
function ca_patrol:evaluation(cfg)
@ -42,15 +115,16 @@ function ca_patrol:execution(cfg)
MAIUV.set_mai_unit_variables(patrol, cfg.ai_id, patrol_vars)
end
while patrol.moves > 0 do
-- Check whether one of the enemies to be attacked is next to the patroller
-- If so, don't move, but attack that enemy
local adjacent_enemy = AH.get_attackable_enemies {
id = cfg.attack,
{ "filter_adjacent", { id = patrol.id } }
}[1]
if adjacent_enemy then break end
-- Check for a possible attack from the patrol's current position first, that
-- way we can skip the other evaluation if one is found
local last_waypoint
if cfg.one_time_only then last_waypoint = waypoints[n_wp] end
local enemy, dst
if (patrol.attacks_left > 0) and (#patrol.attacks > 0) then
enemy, dst = get_best_attack(patrol, patrol.loc, last_waypoint, cfg)
end
while (not enemy) and (patrol.moves > 0) do
-- Also check whether we're next to any unit (enemy or ally) which is on the next waypoint
local unit_on_wp = AH.get_visible_units(wesnoth.current.side, {
x = patrol_vars.patrol_x,
@ -97,7 +171,7 @@ function ca_patrol:execution(cfg)
end
end
-- If we're on the last waypoint on one_time_only is set, stop here
-- If we're on the last waypoint and one_time_only is set, stop here
if cfg.one_time_only and
(patrol.x == waypoints[n_wp][1]) and (patrol.y == waypoints[n_wp][2])
then
@ -106,6 +180,16 @@ function ca_patrol:execution(cfg)
local x, y = wesnoth.find_vacant_tile(patrol_vars.patrol_x, patrol_vars.patrol_y, patrol)
local nh = AH.next_hop(patrol, x, y)
if nh and ((nh[1] ~= patrol.x) or (nh[2] ~= patrol.y)) then
-- Check whether an attackable enemy comes into attack range at any hex along the way
local path = AH.find_path_with_shroud(patrol, nh[1], nh[2])
for i = 2,#path do -- The patrol's current position is already checked above
local loc = path[i]
enemy, dst = get_best_attack(patrol, loc, last_waypoint, cfg)
if enemy then
nh = loc
break
end
end
AH.checked_move(ai, patrol, nh[1], nh[2])
else
AH.checked_stopunit_moves(ai, patrol)
@ -114,28 +198,26 @@ function ca_patrol:execution(cfg)
if (not patrol) or (not patrol.valid) then return end
end
-- Attack unit on the last waypoint under all circumstances if cfg.one_time_only is set
local adjacent_enemy
if cfg.one_time_only then
adjacent_enemy = AH.get_attackable_enemies {
x = waypoints[n_wp][1],
y = waypoints[n_wp][2],
{ "filter_adjacent", { id = patrol.id } }
}[1]
-- It is possible that the patrol unexpectedly ends up next to an enemy, e.g. because of an ambush
if not (enemy) then
enemy, dst = get_best_attack(patrol, patrol.loc, last_waypoint, cfg)
end
-- Otherwise attack adjacent enemy (if specified)
if (not adjacent_enemy) then
adjacent_enemy = AH.get_attackable_enemies {
id = cfg.attack,
{ "filter_adjacent", { id = patrol.id } }
}[1]
-- It is also possible that the patrol cannot make it to 'dst' because of an ambush,
-- in which case we can check whether the ambusher can/should be attacked.
-- So we need to execute the move and the attack separately.
if enemy then
AH.robust_move_and_attack(ai, patrol, dst)
if (not patrol) or (not patrol.valid) then return end
if (patrol.x ~= dst[1]) or (patrol.y ~= dst[2]) then
enemy, dst = get_best_attack(patrol, patrol.loc, last_waypoint, cfg)
end
end
if adjacent_enemy then AH.checked_attack(ai, patrol, adjacent_enemy) end
if (not patrol) or (not patrol.valid) then return end
AH.checked_stopunit_all(ai, patrol)
if enemy then
AH.robust_move_and_attack(ai, patrol, dst, enemy)
end
end
return ca_patrol

View file

@ -8,7 +8,7 @@ function wesnoth.micro_ais.patrol(cfg)
AH.get_multi_named_locs_xy('waypoint', cfg, 'Patrol [micro_ai] tag')
end
local required_keys = {}
local optional_keys = { "id", "[filter]", "attack", "one_time_only", "out_and_back", "waypoint_loc", "waypoint_x", "waypoint_y" }
local optional_keys = { "id", "[filter]", "attack", "attack_range", "attack_invisible_enemies", "one_time_only", "out_and_back", "waypoint_loc", "waypoint_x", "waypoint_y" }
local CA_parms = {
ai_id = 'mai_patrol',
{ ca_id = "move", location = 'ca_patrol.lua', score = cfg.ca_score or 300000 }

View file

@ -1,17 +1,5 @@
#textdomain wesnoth-ai
#define HOME_GUARDIAN X Y
# Meant to be used as a suffix to a unit-generating macro call.
# The previously-generated unit will treat (X,Y) as its home.
[+unit]
[ai]
[vars]
home_loc="loc({X},{Y})"
[/vars]
[/ai]
[/unit]
#enddef
[test]
id=guardians
name= _ "Guardians"
@ -54,26 +42,6 @@
recruit=Orcish Archer,Orcish Grunt
persistent=no
gold=30
[ai]
[modify_ai]
side=1
action=add
# wmllint: unbalanced-on
path=stage[main_loop].candidate_action[]
# wmllint: unbalanced-off
[candidate_action]
engine=fai
name=go_home
id=go_home
type=movement
# wmlindent: start ignoring
evaluation="if( (null != me.vars.home_loc), {AI_CA_MOVE_TO_TARGETS_SCORE}+10, 0)"
action="if( (me.loc != me.vars.home_loc), move(me.loc, next_hop(me.loc, me.vars.home_loc)), move(me.loc, me.loc)#do not move this turn#)"
# wmlindent: stop ignoring
[/candidate_action]
[/modify_ai]
[/ai]
[/side]
# Put all the units and markers out there
@ -264,31 +232,6 @@
return_x,return_y=21,9
[/micro_ai]
# The home guards
[unit]
type=Troll Whelp
side=2
id=home1
name= _ "Home Guard 1"
x,y=19,2
[variables]
label="home 19,2"
[/variables]
[/unit]
{HOME_GUARDIAN 19 2}
[unit]
type=Troll
side=2
id=home 2
name= _ "Home Guard 2"
x,y=21,10
[variables]
label="home 21,10"
[/variables]
[/unit]
{HOME_GUARDIAN 21 10}
# The stationed guardians
[unit]
type=Skeleton Archer
@ -480,23 +423,6 @@ separate attack Zone"
[/message]
[/command]
[/set_menu_item]
[set_menu_item]
id=m03_home
description= _ "Home Guard"
image=units/trolls/grunt.png~CROP(31,7,24,24)
[show_if]
{VARIABLE_CONDITIONAL scenario_name equals guardians}
[/show_if]
[command]
[message]
speaker=narrator
image=portraits/trolls/troll.png
caption= _ "Home Guard"
message= _ "A 'home guard' is a variant on the 'guardian' AI special. With this variant, the unit has an assigned 'home' location, and will return there if not involved in combat and if not going to a village, whether for healing or to capture it this turn. (By contrast, the standard guardian AI will cause the unit to stay where it last attacked.) This differs from 'return guardian' in that a home guard will press the attack, possibly getting drawn quite far from 'home', rather than returning after each attack. (It can also be lured away by a string of closely-placed villages, but that is something a map builder can control.)
This also demonstrates how to combine candidate actions from Formula AI and Lua AI in one side. The home guard is written in Formula AI, while the return and stationed guardians and the cowards are written in Lua AI. In addition the non-guardian units of the side follow the default AI behavior."
[/message]
[/command]
[/set_menu_item]
[set_menu_item]
id=m04_stationed
description= _ "Stationed Guardian"

View file

@ -271,49 +271,13 @@
income=-2
[/side]
# Formula AI Lurkers
[side]
side=6
controller=ai
type=Saurian Oracle
x,y=12,17
max_moves,max_attacks=0,0
persistent=no
team_name=lurkers_fai
user_team_name= _ "Formula AI Lurkers (saurians)"
gold=0
income=-2
[ai]
[stage]
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
[candidate_action]
engine=fai
name=lurker_moves_fai
id=lurker_moves_fai
max_score=300000
type=movement
[filter]
me="filter(input, (self.type = 'Saurian Skirmisher'))"
[/filter]
evaluation=300000
action="{ai/micro_ais/engines/lurker_moves.fai}"
[/candidate_action]
[/stage]
[/ai]
[/side]
[event]
name=preload
first_time_only=no
# Hide the other sides' leaders; they are only there so that side color shows up in Status menu
[hide_unit]
side=2,3,4,5,6
side=2,3,4,5
canrecruit=yes
[/hide_unit]
[/event]
@ -329,7 +293,6 @@
{SCATTER_UNITS 3 "Saurian Skirmisher" 1 (x,terrain=12-19,S*) (side=3)}
{SCATTER_UNITS 3 "Naga Fighter" 1 (y,terrain=18,W*) (side=4)}
{SCATTER_UNITS 3 "Saurian Skirmisher" 1 (x,terrain=21-29,S*) (side=5)}
{SCATTER_UNITS 3 "Saurian Skirmisher" 1 (x,terrain=32-99,S*) (side=6)}
# The Micro AI lurkers
[micro_ai]
@ -376,7 +339,7 @@
[/micro_ai]
# The WML lurkers
{LURKER_MOVES 5 (1,2,3,4,6)}
{LURKER_MOVES 5 (1,2,3,4)}
{PLACE_IMAGE "scenery/signpost.png" 27 3}
{SET_LABEL 27 3 _"End Scenario"}
@ -429,18 +392,6 @@
{UNIT 5 (Saurian Skirmisher) $x1 $y1 ()}
[/command]
[/set_menu_item]
[set_menu_item]
id=m01_menu_lurker6
description= _ "Place a Side 6 lurker"
image=units/saurians/skirmisher/skirmisher.png~CROP(28,25,24,24)
[show_if]
{VARIABLE_CONDITIONAL scenario_name equals lurkers}
[/show_if]
[command]
{UNIT 6 (Saurian Skirmisher) $x1 $y1 ()}
[/command]
[/set_menu_item]
[/event]
# Start
@ -465,7 +416,7 @@ Side 3 (green): saurians attacking only from swamp. If no enemy is in range, the
Side 4 (purple): nagas wandering only on water terrain, but attacking from both water and swamp.
We also added two other sides, which demonstrate lurker behavior coded in WML (Side 5, gray) and Formula AI (Side 6, brown)."
We also added another side, which demonstrates lurker behavior coded in WML (Side 5, gray)."
[/message]
[message]
@ -510,7 +461,7 @@ The Lua Lurker AI is coded as a Micro AI. A Micro AI can be added and adapted to
[if]
[not]
[have_unit]
side=2,3,4,5,6
side=2,3,4,5
[/have_unit]
[/not]
[then]

View file

@ -247,8 +247,8 @@
faction=Custom
[ai]
[stage]
engine=fai
name=unit_formulas
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
[/stage]
[/ai]
[/side]
@ -279,15 +279,19 @@
name=_ "Odrun"
side=7
x,y=25,4
[ai]
loop_formula="{ai/formula/patrol.fai}"
[vars]
guard_radius=3
waypoints=[ loc(25,4) -> loc(14,2), loc(14,2) -> loc(9,12), loc(9,12) -> loc(6,19), loc(6,19) -> loc(25,4) ]
next_step="loc(25,4)"
[/vars]
[/ai]
[/unit]
[micro_ai]
side=7
ai_type=patrol
action=add
[filter]
id=Odrun
[/filter]
waypoint_x=14,9,6,25
waypoint_y=2,12,19,4
attack_range=4
attack_invisible_enemies=yes
[/micro_ai]
[unit]
type=Goblin Pillager
@ -295,15 +299,19 @@
name=_ "Kardur"
side=7
x,y=10,20
[ai]
loop_formula="{ai/formula/patrol.fai}"
[vars]
guard_radius=3
waypoints=[ loc(10,20) -> loc(18,24), loc(18,24) -> loc(21,19), loc(21,19) -> loc(18,11), loc(18,11) -> loc(10,20) ]
next_step="loc(10,20)"
[/vars]
[/ai]
[/unit]
[micro_ai]
side=7
ai_type=patrol
action=add
[filter]
id=Kardur
[/filter]
waypoint_x=18,21,18,10
waypoint_y=24,19,11,20
attack_range=4
attack_invisible_enemies=yes
[/micro_ai]
[unit]
type=Goblin Pillager
@ -311,15 +319,19 @@
name=_ "Kartrog"
side=7
x,y=22,27
[ai]
loop_formula="{ai/formula/patrol.fai}"
[vars]
guard_radius=3
waypoints=[ loc(22,27) -> loc(27,24), loc(27,24) -> loc(21,16),loc(21,16) -> loc(22,27), loc(22,27) -> loc(6,24), loc(6,24)-> loc(22,27)]
next_step="loc(22,27)"
[/vars]
[/ai]
[/unit]
[micro_ai]
side=7
ai_type=patrol
action=add
[filter]
id=Kartrog
[/filter]
waypoint_x=27,21,22,6,22
waypoint_y=24,16,27,24,27
attack_range=4
attack_invisible_enemies=yes
[/micro_ai]
[unit]
type=Goblin Knight
@ -327,15 +339,19 @@
name=_ "Sdrul"
side=7
x,y=13,19
[ai]
loop_formula="{ai/formula/patrol.fai}"
[vars]
guard_radius=3
waypoints=[ loc(13,19) -> loc(16,15), loc(16,15) -> loc(25,4), loc(25,4) -> loc(13,19) ]
next_step="loc(13,19)"
[/vars]
[/ai]
[/unit]
[micro_ai]
side=7
ai_type=patrol
action=add
[filter]
id=Sdrul
[/filter]
waypoint_x=16,25,13
waypoint_y=15,4,19
attack_range=4
attack_invisible_enemies=yes
[/micro_ai]
[unit]
type=Direwolf Rider
@ -343,15 +359,19 @@
name=_ "Utrub"
side=7
x,y=29,24
[ai]
loop_formula="{ai/formula/patrol.fai}"
[vars]
guard_radius=3
waypoints=[ loc(29,24) -> loc(16,15), loc(16,15) -> loc(29,24) ]
next_step="loc(29,24)"
[/vars]
[/ai]
[/unit]
[micro_ai]
side=7
ai_type=patrol
action=add
[filter]
id=Utrub
[/filter]
waypoint_x=16,29
waypoint_y=15,24
attack_range=4
attack_invisible_enemies=yes
[/micro_ai]
[unit]
type=Goblin Knight
@ -359,15 +379,19 @@
name=_ "Uhmit"
side=7
x,y=17,8
[ai]
loop_formula="{ai/formula/patrol.fai}"
[vars]
guard_radius=3
waypoints=[ loc(25,4) -> loc(16,15), loc(16,15) -> loc(25,4) ]
next_step="loc(17,8)"
[/vars]
[/ai]
[/unit]
[micro_ai]
side=7
ai_type=patrol
action=add
[filter]
id=Uhmit
[/filter]
waypoint_x=25,16,17
waypoint_y=4,15,8
attack_range=4
attack_invisible_enemies=yes
[/micro_ai]
#### end of wolves ####

View file

@ -1461,6 +1461,8 @@
{DEPRECATED_KEY id string}
{REQUIRED_KEYS_LOC_OR_XY waypoint string_list s_range_list}
{SIMPLE_KEY attack string_list}
{DEFAULT_KEY attack_range s_int 1}
{DEFAULT_KEY attack_invisible_enemies s_bool no}
{DEFAULT_KEY ca_score s_unsigned 300000}
{DEFAULT_KEY one_time_only s_bool no}
{DEFAULT_KEY out_and_back s_bool no}

View file

@ -1151,7 +1151,7 @@
("y" "x" "terrain"))
("micro_ai"
("avoid_unit" "filter_location_enemy" "mobilize_condition" "avoid" "filter_location_wander" "filter_location" "unit" "probability" "filter_second" "filter")
("weapon" "disable_move_leader_to_keep" "avoid_type" "tusklet_type" "tusker_type" "rabbit_type" "deer_type" "active_side_leader" "leadership_y" "leadership_x" "healer_y" "healer_x" "enemy_y" "enemy_x" "y" "x" "show_messages" "rest_turns" "use_straight_line" "avoid_enemies" "unique_goals" "release_all_units_at_goal" "release_unit_at_goal" "ca_id" "seek_x" "distance" "aggression" "injured_units_only" "stationary" "out_and_back" "attack" "one_time_only" "waypoint_y" "waypoint_x" "id" "skip_low_gold_recruiting" "ca_score" "pack_size" "show_pack_number" "action" "ai_type" "side"))
("weapon" "disable_move_leader_to_keep" "avoid_type" "tusklet_type" "tusker_type" "rabbit_type" "deer_type" "active_side_leader" "leadership_y" "leadership_x" "healer_y" "healer_x" "enemy_y" "enemy_x" "y" "x" "show_messages" "rest_turns" "use_straight_line" "avoid_enemies" "unique_goals" "release_all_units_at_goal" "release_unit_at_goal" "ca_id" "seek_x" "distance" "aggression" "injured_units_only" "stationary" "out_and_back" "attack" "attack_range" "attack_invisible_enemies" "one_time_only" "waypoint_y" "waypoint_x" "id" "skip_low_gold_recruiting" "ca_score" "pack_size" "show_pack_number" "action" "ai_type" "side"))
("petrify" nil
("id"))
("background_layer" nil