Hunter MAI: switch to using external CAs
This commit is contained in:
parent
02aa84e304
commit
f308a26dcb
4 changed files with 191 additions and 197 deletions
|
@ -1,194 +0,0 @@
|
|||
return {
|
||||
init = function(ai, existing_engine)
|
||||
local engine = existing_engine or {}
|
||||
|
||||
local H = wesnoth.require "lua/helper.lua"
|
||||
local W = H.set_wml_action_metatable {}
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
function engine:mai_hunter_attack_weakest_adj_enemy(unit)
|
||||
-- Attack the enemy with the fewest hitpoints adjacent to 'unit', if there is one
|
||||
-- Returns status of the attack:
|
||||
-- 'attacked': if a unit was attacked
|
||||
-- 'killed': if a unit was killed
|
||||
-- 'no_attack': if no unit was attacked
|
||||
|
||||
-- First check that the unit exists and has attacks left
|
||||
if (not unit.valid) then return 'no_attack' end
|
||||
if (unit.attacks_left == 0) then return 'no_attack' end
|
||||
|
||||
local min_hp, target = 9e99, {}
|
||||
for x, y in H.adjacent_tiles(unit.x, unit.y) do
|
||||
local enemy = wesnoth.get_unit(x, y)
|
||||
if enemy and wesnoth.is_enemy(enemy.side, wesnoth.current.side) then
|
||||
if (enemy.hitpoints < min_hp) then
|
||||
min_hp, target = enemy.hitpoints, enemy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if target.id then
|
||||
--W.message { speaker = unit.id, message = 'Attacking weakest adjacent enemy' }
|
||||
ai.attack(unit, target)
|
||||
if target.valid then
|
||||
return 'attacked'
|
||||
else
|
||||
return 'killed'
|
||||
end
|
||||
end
|
||||
|
||||
return 'no_attack'
|
||||
end
|
||||
|
||||
function engine:mai_hunter_eval(cfg)
|
||||
local unit = wesnoth.get_units { side = wesnoth.current.side, id = cfg.id,
|
||||
formula = '$this_unit.moves > 0'
|
||||
}[1]
|
||||
|
||||
if unit then return cfg.ca_score end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- cfg parameters: id, hunting_ground, home_x, home_y, rest_turns, show_messages
|
||||
function engine:mai_hunter_exec(cfg)
|
||||
-- Unit with the given ID goes on a hunt, doing a random wander in area given by
|
||||
-- hunting_ground, then retreats to
|
||||
-- position given by 'home_x,home_y' for 'rest_turns' turns, or until fully healed
|
||||
|
||||
local unit = wesnoth.get_units { side = wesnoth.current.side, id = cfg.id,
|
||||
formula = '$this_unit.moves > 0'
|
||||
}[1]
|
||||
--print('Hunter: ', unit.id)
|
||||
|
||||
-- If hunting_status is not set for the unit -> default behavior -> hunting
|
||||
if (not unit.variables.hunting_status) then
|
||||
-- Unit gets a new goal if none exist or on any move with 10% random chance
|
||||
local r = AH.random(10)
|
||||
if (not unit.variables.goal_x) or (r <= 1) then
|
||||
-- 'locs' includes border hexes, but that does not matter here
|
||||
locs = AH.get_passable_locations((cfg.filter_location or {}), unit)
|
||||
local rand = AH.random(#locs)
|
||||
--print('#locs', #locs, rand)
|
||||
unit.variables.goal_x, unit.variables.goal_y = locs[rand][1], locs[rand][2]
|
||||
end
|
||||
--print('Hunter goto: ', unit.variables.goal_x, unit.variables.goal_y, r)
|
||||
|
||||
-- Hexes the unit can reach
|
||||
local reach_map = AH.get_reachable_unocc(unit)
|
||||
|
||||
-- Now find the one of these hexes that is closest to the goal
|
||||
local max_rating, best_hex = -9e99, {}
|
||||
reach_map:iter( function(x, y, v)
|
||||
-- Distance from goal is first rating
|
||||
local rating = - H.distance_between(x, y, unit.variables.goal_x, unit.variables.goal_y)
|
||||
|
||||
-- Proximity to an enemy unit is a plus
|
||||
local enemy_hp = 500
|
||||
for xa, ya in H.adjacent_tiles(x, y) do
|
||||
local enemy = wesnoth.get_unit(xa, ya)
|
||||
if enemy and wesnoth.is_enemy(enemy.side, wesnoth.current.side) then
|
||||
if (enemy.hitpoints < enemy_hp) then enemy_hp = enemy.hitpoints end
|
||||
end
|
||||
end
|
||||
rating = rating + 500 - enemy_hp -- prefer attack on weakest enemy
|
||||
|
||||
reach_map:insert(x, y, rating)
|
||||
if (rating > max_rating) then
|
||||
max_rating, best_hex = rating, { x, y }
|
||||
end
|
||||
end)
|
||||
--print(' best_hex: ', best_hex[1], best_hex[2])
|
||||
--AH.put_labels(reach_map)
|
||||
|
||||
if (best_hex[1] ~= unit.x) or (best_hex[2] ~= unit.y) then
|
||||
ai.move(unit, best_hex[1], best_hex[2]) -- partial move only
|
||||
else -- If hunter did not move, we need to stop it (also delete the goal)
|
||||
ai.stopunit_moves(unit)
|
||||
unit.variables.goal_x, unit.variables.goal_y = nil, nil
|
||||
end
|
||||
|
||||
-- Or if this gets the unit to the goal, we also delete the goal
|
||||
if (unit.x == unit.variables.goal_x) and (unit.y == unit.variables.goal_y) then
|
||||
unit.variables.goal_x, unit.variables.goal_y = nil, nil
|
||||
end
|
||||
|
||||
-- Finally, if the unit ended up next to enemies, attack the weakest of those
|
||||
local attack_status = self:mai_hunter_attack_weakest_adj_enemy(unit)
|
||||
|
||||
-- If the enemy was killed, hunter returns home
|
||||
if unit.valid and (attack_status == 'killed') then
|
||||
unit.variables.goal_x, unit.variables.goal_y = nil, nil
|
||||
unit.variables.hunting_status = 'return'
|
||||
if cfg.show_messages then
|
||||
W.message { speaker = unit.id, message = 'Now that I have eaten, I will go back home.' }
|
||||
end
|
||||
end
|
||||
|
||||
-- At this point, issue a 'return', so that no other action takes place this turn
|
||||
return
|
||||
end
|
||||
|
||||
-- If we got here, this means the unit is either returning, or resting
|
||||
if (unit.variables.hunting_status == 'return') then
|
||||
goto_x, goto_y = wesnoth.find_vacant_tile(cfg.home_x, cfg.home_y)
|
||||
--print('Go home:', home_x, home_y, goto_x, goto_y)
|
||||
|
||||
local next_hop = AH.next_hop(unit, goto_x, goto_y)
|
||||
if next_hop then
|
||||
--print(next_hop[1], next_hop[2])
|
||||
AH.movefull_stopunit(ai, unit, next_hop)
|
||||
|
||||
-- If there's an enemy on the 'home' hex and we got right next to it, attack that enemy
|
||||
if (H.distance_between(cfg.home_x, cfg.home_y, next_hop[1], next_hop[2]) == 1) then
|
||||
local enemy = wesnoth.get_unit(cfg.home_x, cfg.home_y)
|
||||
if enemy and wesnoth.is_enemy(enemy.side, unit.side) then
|
||||
if cfg.show_messages then
|
||||
W.message { speaker = unit.id, message = 'Get out of my home!' }
|
||||
end
|
||||
ai.attack(unit, enemy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- We also attack the weakest adjacent enemy, if still possible
|
||||
self:mai_hunter_attack_weakest_adj_enemy(unit)
|
||||
|
||||
-- If the unit got home, start the resting counter
|
||||
if unit.valid and (unit.x == cfg.home_x) and (unit.y == cfg.home_y) then
|
||||
unit.variables.hunting_status = 'resting'
|
||||
unit.variables.resting_until = wesnoth.current.turn + (cfg.rest_turns or 3)
|
||||
if cfg.show_messages then
|
||||
W.message { speaker = unit.id, message = 'I made it home - resting now until the end of Turn ' .. unit.variables.resting_until .. ' or until fully healed.' }
|
||||
end
|
||||
end
|
||||
|
||||
-- At this point, issue a 'return', so that no other action takes place this turn
|
||||
return
|
||||
end
|
||||
|
||||
-- If we got here, the only remaining action is resting
|
||||
if (unit.variables.hunting_status == 'resting') then
|
||||
-- So all we need to do is take moves away from the unit
|
||||
ai.stopunit_moves(unit)
|
||||
|
||||
-- However, we do also attack the weakest adjacent enemy, if still possible
|
||||
self:mai_hunter_attack_weakest_adj_enemy(unit)
|
||||
|
||||
-- If this is the last turn of resting, we also remove the status and turn variable
|
||||
if unit.valid and (unit.hitpoints >= unit.max_hitpoints) and (unit.variables.resting_until <= wesnoth.current.turn) then
|
||||
unit.variables.hunting_status = nil
|
||||
unit.variables.resting_until = nil
|
||||
if cfg.show_messages then
|
||||
W.message { speaker = unit.id, message = 'I am done resting. It is time to go hunting again next turn.' }
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- In principle we should never get here, but just in case: reset variable, so that unit goes hunting on next turn
|
||||
unit.variables.hunting_status = nil
|
||||
end
|
||||
|
||||
return engine
|
||||
end
|
||||
}
|
|
@ -403,7 +403,7 @@ function wesnoth.wml_actions.micro_ai(cfg)
|
|||
elseif (cfg.ai_type == 'hunter') then
|
||||
required_keys = { "id", "home_x", "home_y" }
|
||||
optional_keys = { "filter_location", "rest_turns", "show_messages" }
|
||||
CA_parms = { { ca_id = "mai_hunter", score = cfg.ca_score or 300000 } }
|
||||
CA_parms = { { ca_id = "mai_hunter", location = 'ai/micro_ais/cas/ca_hunter.lua', score = cfg.ca_score or 300000 } }
|
||||
|
||||
--------- Patrol Micro AI ------------------------------------
|
||||
elseif (cfg.ai_type == 'patrol') then
|
||||
|
|
190
data/ai/micro_ais/cas/ca_hunter.lua
Normal file
190
data/ai/micro_ais/cas/ca_hunter.lua
Normal file
|
@ -0,0 +1,190 @@
|
|||
local H = wesnoth.require "lua/helper.lua"
|
||||
local W = H.set_wml_action_metatable {}
|
||||
local AH = wesnoth.require "ai/lua/ai_helper.lua"
|
||||
|
||||
local ca_hunter = {}
|
||||
|
||||
local function hunter_attack_weakest_adj_enemy(ai, unit)
|
||||
-- Attack the enemy with the fewest hitpoints adjacent to 'unit', if there is one
|
||||
-- Returns status of the attack:
|
||||
-- 'attacked': if a unit was attacked
|
||||
-- 'killed': if a unit was killed
|
||||
-- 'no_attack': if no unit was attacked
|
||||
|
||||
-- First check that the unit exists and has attacks left
|
||||
if (not unit.valid) then return 'no_attack' end
|
||||
if (unit.attacks_left == 0) then return 'no_attack' end
|
||||
|
||||
local min_hp, target = 9e99, {}
|
||||
for x, y in H.adjacent_tiles(unit.x, unit.y) do
|
||||
local enemy = wesnoth.get_unit(x, y)
|
||||
if enemy and wesnoth.is_enemy(enemy.side, wesnoth.current.side) then
|
||||
if (enemy.hitpoints < min_hp) then
|
||||
min_hp, target = enemy.hitpoints, enemy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if target.id then
|
||||
--W.message { speaker = unit.id, message = 'Attacking weakest adjacent enemy' }
|
||||
ai.attack(unit, target)
|
||||
if target.valid then
|
||||
return 'attacked'
|
||||
else
|
||||
return 'killed'
|
||||
end
|
||||
end
|
||||
|
||||
return 'no_attack'
|
||||
end
|
||||
|
||||
function ca_hunter:evaluation(ai, cfg)
|
||||
local unit = wesnoth.get_units { side = wesnoth.current.side, id = cfg.id,
|
||||
formula = '$this_unit.moves > 0'
|
||||
}[1]
|
||||
|
||||
if unit then return cfg.ca_score end
|
||||
return 0
|
||||
end
|
||||
|
||||
-- cfg parameters: id, hunting_ground, home_x, home_y, rest_turns, show_messages
|
||||
function ca_hunter:execution(ai, cfg)
|
||||
-- Unit with the given ID goes on a hunt, doing a random wander in area given by
|
||||
-- hunting_ground, then retreats to
|
||||
-- position given by 'home_x,home_y' for 'rest_turns' turns, or until fully healed
|
||||
|
||||
local unit = wesnoth.get_units { side = wesnoth.current.side, id = cfg.id,
|
||||
formula = '$this_unit.moves > 0'
|
||||
}[1]
|
||||
--print('Hunter: ', unit.id)
|
||||
|
||||
-- If hunting_status is not set for the unit -> default behavior -> hunting
|
||||
if (not unit.variables.hunting_status) then
|
||||
-- Unit gets a new goal if none exist or on any move with 10% random chance
|
||||
local r = AH.random(10)
|
||||
if (not unit.variables.goal_x) or (r <= 1) then
|
||||
-- 'locs' includes border hexes, but that does not matter here
|
||||
locs = AH.get_passable_locations((cfg.filter_location or {}), unit)
|
||||
local rand = AH.random(#locs)
|
||||
--print('#locs', #locs, rand)
|
||||
unit.variables.goal_x, unit.variables.goal_y = locs[rand][1], locs[rand][2]
|
||||
end
|
||||
--print('Hunter goto: ', unit.variables.goal_x, unit.variables.goal_y, r)
|
||||
|
||||
-- Hexes the unit can reach
|
||||
local reach_map = AH.get_reachable_unocc(unit)
|
||||
|
||||
-- Now find the one of these hexes that is closest to the goal
|
||||
local max_rating, best_hex = -9e99, {}
|
||||
reach_map:iter( function(x, y, v)
|
||||
-- Distance from goal is first rating
|
||||
local rating = - H.distance_between(x, y, unit.variables.goal_x, unit.variables.goal_y)
|
||||
|
||||
-- Proximity to an enemy unit is a plus
|
||||
local enemy_hp = 500
|
||||
for xa, ya in H.adjacent_tiles(x, y) do
|
||||
local enemy = wesnoth.get_unit(xa, ya)
|
||||
if enemy and wesnoth.is_enemy(enemy.side, wesnoth.current.side) then
|
||||
if (enemy.hitpoints < enemy_hp) then enemy_hp = enemy.hitpoints end
|
||||
end
|
||||
end
|
||||
rating = rating + 500 - enemy_hp -- prefer attack on weakest enemy
|
||||
|
||||
reach_map:insert(x, y, rating)
|
||||
if (rating > max_rating) then
|
||||
max_rating, best_hex = rating, { x, y }
|
||||
end
|
||||
end)
|
||||
--print(' best_hex: ', best_hex[1], best_hex[2])
|
||||
--AH.put_labels(reach_map)
|
||||
|
||||
if (best_hex[1] ~= unit.x) or (best_hex[2] ~= unit.y) then
|
||||
ai.move(unit, best_hex[1], best_hex[2]) -- partial move only
|
||||
else -- If hunter did not move, we need to stop it (also delete the goal)
|
||||
ai.stopunit_moves(unit)
|
||||
unit.variables.goal_x, unit.variables.goal_y = nil, nil
|
||||
end
|
||||
|
||||
-- Or if this gets the unit to the goal, we also delete the goal
|
||||
if (unit.x == unit.variables.goal_x) and (unit.y == unit.variables.goal_y) then
|
||||
unit.variables.goal_x, unit.variables.goal_y = nil, nil
|
||||
end
|
||||
|
||||
-- Finally, if the unit ended up next to enemies, attack the weakest of those
|
||||
local attack_status = hunter_attack_weakest_adj_enemy(ai, unit)
|
||||
|
||||
-- If the enemy was killed, hunter returns home
|
||||
if unit.valid and (attack_status == 'killed') then
|
||||
unit.variables.goal_x, unit.variables.goal_y = nil, nil
|
||||
unit.variables.hunting_status = 'return'
|
||||
if cfg.show_messages then
|
||||
W.message { speaker = unit.id, message = 'Now that I have eaten, I will go back home.' }
|
||||
end
|
||||
end
|
||||
|
||||
-- At this point, issue a 'return', so that no other action takes place this turn
|
||||
return
|
||||
end
|
||||
|
||||
-- If we got here, this means the unit is either returning, or resting
|
||||
if (unit.variables.hunting_status == 'return') then
|
||||
goto_x, goto_y = wesnoth.find_vacant_tile(cfg.home_x, cfg.home_y)
|
||||
--print('Go home:', home_x, home_y, goto_x, goto_y)
|
||||
|
||||
local next_hop = AH.next_hop(unit, goto_x, goto_y)
|
||||
if next_hop then
|
||||
--print(next_hop[1], next_hop[2])
|
||||
AH.movefull_stopunit(ai, unit, next_hop)
|
||||
|
||||
-- If there's an enemy on the 'home' hex and we got right next to it, attack that enemy
|
||||
if (H.distance_between(cfg.home_x, cfg.home_y, next_hop[1], next_hop[2]) == 1) then
|
||||
local enemy = wesnoth.get_unit(cfg.home_x, cfg.home_y)
|
||||
if enemy and wesnoth.is_enemy(enemy.side, unit.side) then
|
||||
if cfg.show_messages then
|
||||
W.message { speaker = unit.id, message = 'Get out of my home!' }
|
||||
end
|
||||
ai.attack(unit, enemy)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- We also attack the weakest adjacent enemy, if still possible
|
||||
hunter_attack_weakest_adj_enemy(ai, unit)
|
||||
|
||||
-- If the unit got home, start the resting counter
|
||||
if unit.valid and (unit.x == cfg.home_x) and (unit.y == cfg.home_y) then
|
||||
unit.variables.hunting_status = 'resting'
|
||||
unit.variables.resting_until = wesnoth.current.turn + (cfg.rest_turns or 3)
|
||||
if cfg.show_messages then
|
||||
W.message { speaker = unit.id, message = 'I made it home - resting now until the end of Turn ' .. unit.variables.resting_until .. ' or until fully healed.' }
|
||||
end
|
||||
end
|
||||
|
||||
-- At this point, issue a 'return', so that no other action takes place this turn
|
||||
return
|
||||
end
|
||||
|
||||
-- If we got here, the only remaining action is resting
|
||||
if (unit.variables.hunting_status == 'resting') then
|
||||
-- So all we need to do is take moves away from the unit
|
||||
ai.stopunit_moves(unit)
|
||||
|
||||
-- However, we do also attack the weakest adjacent enemy, if still possible
|
||||
hunter_attack_weakest_adj_enemy(ai, unit)
|
||||
|
||||
-- If this is the last turn of resting, we also remove the status and turn variable
|
||||
if unit.valid and (unit.hitpoints >= unit.max_hitpoints) and (unit.variables.resting_until <= wesnoth.current.turn) then
|
||||
unit.variables.hunting_status = nil
|
||||
unit.variables.resting_until = nil
|
||||
if cfg.show_messages then
|
||||
W.message { speaker = unit.id, message = 'I am done resting. It is time to go hunting again next turn.' }
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- In principle we should never get here, but just in case: reset variable, so that unit goes hunting on next turn
|
||||
unit.variables.hunting_status = nil
|
||||
end
|
||||
|
||||
return ca_hunter
|
|
@ -21,8 +21,6 @@
|
|||
persistent=no
|
||||
|
||||
gold=200
|
||||
|
||||
{MICRO_AI_HUNTER}
|
||||
[/side]
|
||||
|
||||
[side]
|
||||
|
|
Loading…
Add table
Reference in a new issue