New Micro AI: Assassin Squad AI
This commit is contained in:
parent
e0834cf58b
commit
f7d9d747c0
3 changed files with 180 additions and 1 deletions
|
@ -1,4 +1,6 @@
|
|||
Version 1.13.5+dev:
|
||||
* AI:
|
||||
* New Micro AI: Assassin Squad AI
|
||||
* Campaigns:
|
||||
* Eastern Invasion:
|
||||
* Fixed broken village encounters.
|
||||
|
@ -3623,7 +3625,7 @@ Version 1.9.7:
|
|||
upon editing this text.
|
||||
* WML engine:
|
||||
* added mode=replace to [modify_unit] to replace rather than merge unit subtags
|
||||
(does not apply to object, trait, effect, or advancement)
|
||||
(does not apply to object, trait, effect, or advancement)
|
||||
* new attribute team_name= in SSFs
|
||||
* added [event][filter_side]<SSF keys> support
|
||||
* added support for inline SSF to [chat]
|
||||
|
|
167
data/ai/micro_ais/cas/ca_assassin_move.lua
Normal file
167
data/ai/micro_ais/cas/ca_assassin_move.lua
Normal file
|
@ -0,0 +1,167 @@
|
|||
local H = wesnoth.require "lua/helper.lua"
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
local LS = wesnoth.require "lua/location_set.lua"
|
||||
|
||||
local function get_units_target(cfg)
|
||||
local units = AH.get_units_with_moves {
|
||||
side = wesnoth.current.side,
|
||||
{ "and", H.get_child(cfg, "filter") }
|
||||
}
|
||||
|
||||
local target = wesnoth.get_units {
|
||||
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } },
|
||||
{ "and", H.get_child(cfg, "filter_second") }
|
||||
}[1]
|
||||
|
||||
return units, target
|
||||
end
|
||||
|
||||
local function custom_cost(x, y, unit, enemy_rating_map, prefer_map)
|
||||
-- Custom cost function for assassin path finding consisting of:
|
||||
-- 1. The standard movecost of the units
|
||||
-- 2. A penalty for hexes that can be attacked or are blocked by enemies (stored in rating map)
|
||||
-- 3. A penalty for non-preferred hexes (prefer_map). This has to be a penalty for
|
||||
-- non-preferred hexes rather than a bonus for preferred hexes as the cost function
|
||||
-- must return values >=1 for the a* search to work.
|
||||
|
||||
local terrain = wesnoth.get_terrain(x, y)
|
||||
local move_cost = wesnoth.unit_movement_cost(unit, terrain)
|
||||
|
||||
move_cost = move_cost + (enemy_rating_map:get(x, y) or 0)
|
||||
|
||||
if prefer_map then
|
||||
if (not prefer_map:get(x, y)) then
|
||||
move_cost = move_cost + 5
|
||||
end
|
||||
end
|
||||
|
||||
return move_cost
|
||||
end
|
||||
|
||||
local ca_assassin_move = {}
|
||||
|
||||
function ca_assassin_move:evaluation(cfg, data)
|
||||
local units, target = get_units_target(cfg)
|
||||
if (not units[1]) then return 0 end
|
||||
if (not target) then return 0 end
|
||||
|
||||
return cfg.ca_score
|
||||
end
|
||||
|
||||
function ca_assassin_move:execution(cfg, data)
|
||||
-- We simply move the assassins one at a time
|
||||
local units, target = get_units_target(cfg)
|
||||
local unit = units[1]
|
||||
|
||||
local enemies = wesnoth.get_units {
|
||||
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } },
|
||||
{ "not", H.get_child(cfg, "filter_second") }
|
||||
}
|
||||
|
||||
-- Maximum damage the enemies can theoretically do for all hexes they can attack
|
||||
local enemy_damage_map = LS.create()
|
||||
for _,enemy in ipairs(enemies) do
|
||||
-- Need to "move" enemy next to unit for attack calculation
|
||||
-- Do this with a unit copy, so that no actual unit has to be moved
|
||||
local enemy_copy = wesnoth.copy_unit(enemy)
|
||||
|
||||
-- First get the reach of the enemy with full moves though
|
||||
enemy_copy.moves = enemy_copy.max_moves
|
||||
local reach = wesnoth.find_reach(enemy_copy, { ignore_units = true })
|
||||
|
||||
enemy_copy.x = unit.x
|
||||
enemy_copy.y = unit.y + 1 -- this even works at map border
|
||||
|
||||
local _, _, att_weapon, _ = wesnoth.simulate_combat(enemy_copy, unit)
|
||||
local max_damage = att_weapon.damage * att_weapon.num_blows
|
||||
|
||||
local unit_damage_map = LS.create()
|
||||
for _,loc in ipairs(reach) do
|
||||
unit_damage_map:insert(loc[1], loc[2], max_damage)
|
||||
for xa,ya in H.adjacent_tiles(loc[1], loc[2]) do
|
||||
unit_damage_map:insert(xa, ya, max_damage)
|
||||
end
|
||||
end
|
||||
|
||||
enemy_damage_map:union_merge(unit_damage_map, function(x, y, v1, v2)
|
||||
return (v1 or 0) + v2
|
||||
end)
|
||||
end
|
||||
|
||||
-- Penalties for damage by enemies
|
||||
local enemy_rating_map = LS.create()
|
||||
enemy_damage_map:iter(function(x, y, enemy_damage)
|
||||
local hit_chance = (wesnoth.unit_defense(unit, wesnoth.get_terrain(x, y))) / 100.
|
||||
|
||||
local rating = hit_chance * enemy_damage
|
||||
rating = rating / unit.max_hitpoints
|
||||
rating = rating * 5
|
||||
|
||||
enemy_rating_map:insert(x, y, rating)
|
||||
end)
|
||||
|
||||
-- Penalties for blocked hexes and ZOC
|
||||
local is_skirmisher = wesnoth.unit_ability(unit, "skirmisher")
|
||||
for _,enemy in ipairs(enemies) do
|
||||
-- Hexes an enemy is on get a very large penalty
|
||||
enemy_rating_map:insert(enemy.x, enemy.y, (enemy_rating_map:get(enemy.x, enemy.y) or 0) + 100)
|
||||
|
||||
-- Hexes adjacent to enemies get max_moves penalty
|
||||
-- except if AI unit is skirmisher or enemy is level 0
|
||||
local zoc_active = (not is_skirmisher)
|
||||
|
||||
if zoc_active then
|
||||
local level = wesnoth.unit_types[enemy.type].level
|
||||
if (level == 0) then zoc_active = false end
|
||||
end
|
||||
|
||||
if zoc_active then
|
||||
for xa,ya in H.adjacent_tiles(enemy.x, enemy.y) do
|
||||
enemy_rating_map:insert(xa, ya, (enemy_rating_map:get(xa, ya) or 0) + unit.max_moves)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Preferred hexes (do this here once for all hexes, so that it does not need
|
||||
-- to get done for every step of the a* search.
|
||||
-- We only need to know whether a hex is preferred or not, there's no additional rating.
|
||||
local prefer_slf = H.get_child(cfg, "prefer")
|
||||
local prefer_map -- want this to be nil, not empty LS if [prefer] tag not given
|
||||
if prefer_slf then
|
||||
local preferred_hexes = wesnoth.get_locations(prefer_slf)
|
||||
prefer_map = LS.create()
|
||||
for _,hex in ipairs(preferred_hexes) do
|
||||
prefer_map:insert(hex[1], hex[2], true)
|
||||
end
|
||||
end
|
||||
|
||||
local path, cost = wesnoth.find_path(unit, target.x, target.y,
|
||||
function(x, y, current_cost)
|
||||
return custom_cost(x, y, unit, enemy_rating_map, prefer_map)
|
||||
end
|
||||
)
|
||||
|
||||
local path_map = LS.of_pairs(path)
|
||||
|
||||
-- We need to pick the farthest reachable hex along that path
|
||||
local farthest_hex = path[1]
|
||||
for i = 2,#path do
|
||||
local sub_path, sub_cost = wesnoth.find_path(unit, path[i][1], path[i][2], cfg)
|
||||
if sub_cost <= unit.moves then
|
||||
local unit_in_way = wesnoth.get_unit(path[i][1], path[i][2])
|
||||
if not unit_in_way then
|
||||
farthest_hex = path[i]
|
||||
end
|
||||
else
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if farthest_hex then
|
||||
AH.checked_move_full(ai, unit, farthest_hex[1], farthest_hex[2])
|
||||
else
|
||||
AH.checked_stopunit_moves(ai, unit)
|
||||
end
|
||||
end
|
||||
|
||||
return ca_assassin_move
|
|
@ -1,3 +1,13 @@
|
|||
function wesnoth.micro_ais.assassin(cfg)
|
||||
local required_keys = { "[filter]", "[filter_second]" }
|
||||
local optional_keys = { "[prefer]" }
|
||||
local CA_parms = {
|
||||
ai_id = 'mai_assassin',
|
||||
{ ca_id = 'attack', location = 'ca_simple_attack.lua', score = 110001 },
|
||||
{ ca_id = 'move', location = 'ca_assassin_move.lua', score = 110000 }
|
||||
}
|
||||
return required_keys, optional_keys, CA_parms
|
||||
end
|
||||
|
||||
function wesnoth.micro_ais.lurkers(cfg)
|
||||
local required_keys = { "[filter]", "[filter_location]" }
|
||||
|
|
Loading…
Add table
Reference in a new issue