New Micro AI: Fast AI

This AI is meant as a fall-back AI for scenarios where there are so
many units that attacks and moves done by the default AI are slow with
long delays before each move. It replaces the default combat and
move-to-targets candidate actions.
See http://wiki.wesnoth.org/Micro_AIs
This commit is contained in:
mattsc 2014-05-03 16:52:53 -07:00
parent 8d428e06c3
commit feabdfb4ea
4 changed files with 422 additions and 1 deletions

View file

@ -1,5 +1,6 @@
Version 1.13.0-dev:
* AI:
* New Micro AI: Fast AI
* Messenger Escort Micro AI: new optional parameters [filter],
[filter_second] and invert_order=
* Messenger Escort Micro AI: bug fix for escort units blocking messenger's
@ -113,7 +114,7 @@ Version 1.13.0-dev:
* Fix bug #21883: make sure movement animations don't cycle with fog on
* Fix bug #21316: make subframes within standing animations cycle by default
* Fix bug #21967: fix crash when unit modification to traits has empty id
* Fix bug #19258, 21962: WML variables spuriously copied to replay_start
* Fix bug #19258, 21962: WML variables spuriously copied to replay_start
* Fix implementation bug in random number generator: rand_pool_ is now an
unsigned long rather than an unsigned int.
* Fix bug #21491: fix drag+drop for unit movements

View file

@ -0,0 +1,77 @@
local H = wesnoth.require "lua/helper.lua"
local AH = wesnoth.require "ai/lua/ai_helper.lua"
local BC = wesnoth.require "ai/lua/battle_calcs.lua"
local ca_fast_combat = {}
function ca_fast_combat:evaluation(ai, cfg, self)
if (not self.data.fast_cache) or (self.data.fast_cache.turn ~= wesnoth.current.turn) then
self.data.fast_cache = { turn = wesnoth.current.turn }
end
if (not self.data.fast_combat_units) or (not self.data.fast_combat_units[1]) then
--print('Reusing unit table')
self.data.fast_combat_units = wesnoth.get_units { side = wesnoth.current.side }
if (not self.data.fast_combat_units[1]) then return 0 end
-- For speed reasons, we'll go through the arrays from the end, so they are sorted backwards
if cfg.weak_units_first then
table.sort(self.data.fast_combat_units, function(a,b) return a.hitpoints > b.hitpoints end)
else
table.sort(self.data.fast_combat_units, function(a,b) return a.hitpoints < b.hitpoints end)
end
end
local aggression = ai.get_aggression()
if (aggression > 1) then aggression = 1 end
--print('aggression', aggression)
for i = #self.data.fast_combat_units,1,-1 do
local unit = self.data.fast_combat_units[i]
--print('Considering unit ' .. i .. ': ' .. unit.id)
if (unit.attacks_left > 0) and (H.get_child(unit.__cfg, 'attack')) then
local attacks = AH.get_attacks({ unit }, { include_occupied = cfg.include_occupied_attack_hexes })
if (#attacks > 0) then
local max_rating, best_target, best_dst = -9e99
for _,attack in ipairs(attacks) do
local target = wesnoth.get_unit(attack.target.x, attack.target.y)
local rating = BC.attack_rating(
unit, target, { attack.dst.x, attack.dst.y },
{ own_value_weight = 1.0 - aggression },
self.data.fast_cache
)
--print(unit.id, target.id, rating)
if (rating > 0) and (rating > max_rating) then
max_rating, best_target, best_dst = rating, target, attack.dst
end
end
if best_target then
self.data.fast_combat_unit_i = i
self.data.fast_target, self.data.fast_dst = best_target, best_dst
return cfg.ca_score
end
end
end
self.data.fast_combat_units[i] = nil
end
return 0
end
function ca_fast_combat:execution(ai, cfg, self)
local unit = self.data.fast_combat_units[self.data.fast_combat_unit_i]
AH.movefull_outofway_stopunit(ai, unit, self.data.fast_dst.x, self.data.fast_dst.y)
if unit and unit.valid and self.data.fast_target and self.data.fast_target.valid then
AH.checked_attack(ai, unit, self.data.fast_target)
end
self.data.fast_combat_units[self.data.fast_combat_unit_i] = nil
end
return ca_fast_combat

View file

@ -0,0 +1,234 @@
local H = wesnoth.require "lua/helper.lua"
local AH = wesnoth.dofile "ai/lua/ai_helper.lua"
local ca_fast_move = {}
function ca_fast_move:evaluation(ai, cfg, self)
local unit = AH.get_units_with_moves { side = wesnoth.current.side, canrecruit = 'no' }[1]
if unit then return 20000 end
return 0
end
function ca_fast_move:execution(ai, cfg, self)
local move_cost_factor = cfg.move_cost_factor or 2.0
if (move_cost_factor < 1.1) then move_cost_factor = 1.1 end
local units = AH.get_units_with_moves { side = wesnoth.current.side, canrecruit = 'no' }
local leader = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[1]
local goals = {}
-- Villages get added first, so that (hopefully, scouts and faster units will go for them first)
local village_value = ai.get_village_value()
if (village_value > 0) then
local villages = wesnoth.get_villages()
--print('#villages', #villages)
-- Eliminate villages owned by a side that is not an enemy
for i = #villages,1,-1 do
local owner = wesnoth.get_village_owner(villages[i][1], villages[i][2])
if owner and (not wesnoth.is_enemy(owner, wesnoth.current.side)) then
table.remove(villages, i)
end
end
--print('#villages up for grabs', #villages)
-- Add rating that is sum of inverse distances from all other villages
for i = 1,#villages-1 do
local v1 = villages[i]
for j = i+1,#villages do
local v2 = villages[j]
local dist = H.distance_between(v1[1], v1[2], v2[1], v2[2])
dist = math.ceil(dist / 5.) -- In discrete steps of 5 hexes
v1.rating = (v1.rating or 0) + 1. / dist
v2.rating = (v2.rating or 0) + 1. / dist
end
end
-- Multiply by distance from side leader
for _,village in ipairs(villages) do
local dist = 1 -- Just in case there is no leader
dist = math.ceil(dist / 5.) -- In discrete steps of 5 hexes
if leader then
dist = H.distance_between(village[1], village[2], leader.x, leader.y)
end
village.rating = (village.rating or 1.) * 1. / dist
end
-- Now we figure out how many villages we want to go for
local max_villages = math.floor(#units / 4. * village_value)
--print('max_villages', max_villages)
local villages_to_get = math.min(max_villages, #villages)
--print('villages_to_get', villages_to_get)
for _ = 1,villages_to_get do
-- Sort villages by rating (highest rating last, because that makes table.remove faster)
table.sort(villages, function(a, b) return (a.rating < b.rating) end)
local x,y = villages[#villages][1], villages[#villages][2]
table.insert(goals, { x = x, y = y, is_village = true })
table.remove(villages)
-- Now re-rate the villages, want those farthest from the last selected one first
-- Need to check whether this was the last village
if (#villages > 1) then
local base_rating = villages[#villages].rating
for _,village in ipairs(villages) do
village.rating = village.rating / base_rating
* H.distance_between(x, y, village[1], village[2])
end
end
end
end
--print('Time after setting up village goals:', wesnoth.get_time_stamp() - start_time)
-- Now add enemy leaders to the goals
local enemy_leaders =
wesnoth.get_units {
{ "filter_side", { { "enemy_of", { side = wesnoth.current.side } } } },
canrecruit = 'yes'
}
--print('#enemy_leaders', #enemy_leaders)
-- Sort enemy leaders by distance to AI leader
if leader then
table.sort(enemy_leaders, function(a, b)
local dist_a = H.distance_between(leader.x, leader.y, a.x, a.y)
local dist_b = H.distance_between(leader.x, leader.y, b.x, b.y)
return (dist_a < dist_b)
end)
end
for i_el,enemy_leader in ipairs(enemy_leaders) do
local goal = { x = enemy_leader.x, y = enemy_leader.y }
table.insert(goals, goal)
end
--print('Time after setting up enemy leader goals:', wesnoth.get_time_stamp() - start_time)
-- Putting information about all the units into the goals
-- This is a one-time expense up front, but generally not too bad
for _,goal in ipairs(goals) do
-- Insert information about the units
for i_u,unit in ipairs(units) do
local dist = H.distance_between(unit.x, unit.y, goal.x, goal.y)
goal[i_u] = { dist = dist / unit.max_moves, i_unit = i_u }
end
table.sort(goal, function(a, b) return (a.dist < b.dist) end)
end
--print('Time after adding unit info into goals:', wesnoth.get_time_stamp() - start_time)
local keep_moving, next_goal = true, 0
while keep_moving do
keep_moving = false
next_goal = next_goal + 1
if (next_goal > #goals) then next_goal = 1 end
local goal = goals[next_goal]
local max_rating, best_unit_info = -9e99
for _,unit_info in ipairs(goal) do
if (not unit_info.cost) then
local _,cost =
wesnoth.find_path(
units[unit_info.i_unit].x, units[unit_info.i_unit].y,
goal.x, goal.y,
{ ignore_units = true }
)
cost = cost / units[unit_info.i_unit].max_moves
unit_info.cost = cost
end
if (unit_info.cost < unit_info.dist * move_cost_factor) then
--print('Efficient move found:', units[unit_info.i_unit].x, units[unit_info.i_unit].y, goal.x, goal.y)
best_unit_info = unit_info
break
elseif (unit_info.cost < 1000) then
--print('No efficient move found:', units[unit_info.i_unit].x, units[unit_info.i_unit].y, goal.x, goal.y)
local rating = - unit_info.cost
if (rating > max_rating) then
max_rating, best_unit_info = rating, unit_info
end
end
end
if best_unit_info then
local unit = units[best_unit_info.i_unit]
-- Next hop if there weren't any other units around
local next_hop = AH.next_hop(unit, goal.x, goal.y, { ignore_units = true })
-- Finally find the best move for this unit
local reach = wesnoth.find_reach(unit)
local max_rating, best_hex = -9e99
for _,loc in ipairs(reach) do
local rating = - H.distance_between(loc[1], loc[2], next_hop[1], next_hop[2])
rating = rating - H.distance_between(loc[1], loc[2], goal.x, goal.y) / 2.
local unit_in_way
if (rating > max_rating) then
unit_in_way = wesnoth.get_unit(loc[1], loc[2])
if (unit_in_way == unit) then unit_in_way = nil end
if unit_in_way and (unit_in_way.side == unit.side) then
local reach = AH.get_reachable_unocc(unit_in_way)
if (reach:size() > 1) then
unit_in_way = nil
rating = rating - 0.01
end
end
end
if (rating > max_rating) and (not unit_in_way) then
max_rating, best_hex = rating, { loc[1], loc[2] }
end
end
if best_hex then
--print('Doing move:', unit.x, unit.y, best_hex[1], best_hex[2], goal.x, goal.y)
local dx, dy = goal.x - best_hex[1], goal.y - best_hex[2]
local r = math.sqrt(dx * dx + dy * dy)
dx, dy = dx / r, dy / r
AH.movefull_outofway_stopunit(ai, unit, best_hex[1], best_hex[2], { dx = dx, dy = dy })
end
-- Also remove this unit from all the tables; using table.remove is fine here
for _,tmp_goal in ipairs(goals) do
for i_info,tmp_info in ipairs(tmp_goal) do
if (tmp_info.i_unit == best_unit_info.i_unit) then
table.remove(tmp_goal, i_info)
break
end
end
end
-- Finally, if this was a village, remove the goal (we only do one unit per village
if goal.is_village then
--print('Removing goal (only one unit per village):', goal.x, goal.y)
table.remove(goals, next_goal)
next_goal = next_goal - 1
end
else
--print('Removing goal (no more unit can get there):', goal.x, goal.y)
table.remove(goals, next_goal)
next_goal = next_goal - 1
end
for _,goal in ipairs(goals) do
if goal[1] then
keep_moving = true
break
end
end
end
--print('Overall time:', wesnoth.get_time_stamp() - start_time)
end
return ca_fast_move

View file

@ -453,6 +453,115 @@ function wesnoth.wml_actions.micro_ai(cfg)
{ 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 = {
"include_occupied_attack_hexes", "move_cost_factor",
"weak_units_first", "skip_combat_ca", "skip_move_ca"
}
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 }
}
-- 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') then
table.remove(CA_parms, i)
break
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
H.wml_error("unknown value for ai_type= in [micro_ai]")