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:
parent
8d428e06c3
commit
feabdfb4ea
4 changed files with 422 additions and 1 deletions
|
@ -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
|
||||
|
|
77
data/ai/micro_ais/cas/ca_fast_combat.lua
Normal file
77
data/ai/micro_ais/cas/ca_fast_combat.lua
Normal 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
|
234
data/ai/micro_ais/cas/ca_fast_move.lua
Normal file
234
data/ai/micro_ais/cas/ca_fast_move.lua
Normal 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
|
|
@ -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]")
|
||||
|
|
Loading…
Add table
Reference in a new issue