Skip to content

Commit

Permalink
New Micro AI: Assassin Squad AI
Browse files Browse the repository at this point in the history
  • Loading branch information
mattsc committed Sep 1, 2016
1 parent e0834cf commit f7d9d74
Show file tree
Hide file tree
Showing 3 changed files with 180 additions and 1 deletion.
4 changes: 3 additions & 1 deletion changelog
@@ -1,4 +1,6 @@
Version 1.13.5+dev:
* AI:
* New Micro AI: Assassin Squad AI
* Campaigns:
* Eastern Invasion:
* Fixed broken village encounters.
Expand Down Expand Up @@ -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]
Expand Down
167 changes: 167 additions & 0 deletions data/ai/micro_ais/cas/ca_assassin_move.lua
@@ -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
10 changes: 10 additions & 0 deletions data/ai/micro_ais/mai-defs/misc.lua
@@ -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]" }
Expand Down

0 comments on commit f7d9d74

Please sign in to comment.