Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Experimental AI: convert to using external CAs
(cherry-picked from commit 5474785)
- Loading branch information
Showing
11 changed files
with
690 additions
and
698 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
-------- Castle Switch CA -------------- | ||
|
||
local AH = wesnoth.require "ai/lua/ai_helper.lua" | ||
local M = wesnoth.map | ||
|
||
local function get_reachable_enemy_leaders(unit) | ||
-- We're cheating a little here and also find hidden enemy leaders. That's | ||
-- because a human player could make a pretty good educated guess as to where | ||
-- the enemy leaders are likely to be while the AI does not know how to do that. | ||
local potential_enemy_leaders = AH.get_live_units { canrecruit = 'yes', | ||
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } } | ||
} | ||
local enemy_leaders = {} | ||
for j,e in ipairs(potential_enemy_leaders) do | ||
local path, cost = wesnoth.find_path(unit, e.x, e.y, { ignore_units = true, viewing_side = 0 }) | ||
if cost < AH.no_path then | ||
table.insert(enemy_leaders, e) | ||
end | ||
end | ||
|
||
return enemy_leaders | ||
end | ||
|
||
local ca_castle_switch = {} | ||
|
||
function ca_castle_switch:evaluation(cfg, data) | ||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'castle_switch' | ||
if AH.print_eval() then AH.print_ts(' - Evaluating castle_switch CA:') end | ||
|
||
if ai.aspects.passive_leader then | ||
-- Turn off this CA if the leader is passive | ||
return 0 | ||
end | ||
|
||
local leader = wesnoth.get_units { | ||
side = wesnoth.current.side, | ||
canrecruit = 'yes', | ||
formula = '(movement_left = total_movement) and (hitpoints = max_hitpoints)' | ||
}[1] | ||
if not leader then | ||
-- CA is irrelevant if no leader or the leader may have moved from another CA | ||
data.leader_target = nil | ||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end | ||
return 0 | ||
end | ||
|
||
local cheapest_unit_cost = AH.get_cheapest_recruit_cost() | ||
|
||
if data.leader_target and wesnoth.sides[wesnoth.current.side].gold >= cheapest_unit_cost then | ||
-- make sure move is still valid | ||
local next_hop = AH.next_hop(leader, data.leader_target[1], data.leader_target[2]) | ||
if next_hop and next_hop[1] == data.leader_target[1] | ||
and next_hop[2] == data.leader_target[2] then | ||
return data.leader_score | ||
end | ||
end | ||
|
||
local width,height,border = wesnoth.get_map_size() | ||
local keeps = wesnoth.get_locations { | ||
terrain = 'K*,K*^*,*^K*', -- Keeps | ||
x = '1-'..width, | ||
y = '1-'..height, | ||
{ "not", { {"filter", {}} }}, -- That have no unit | ||
{ "not", { radius = 6, {"filter", { canrecruit = 'yes', | ||
{ "filter_side", { { "enemy_of", {side = wesnoth.current.side} } } } | ||
}} }}, -- That are not too close to an enemy leader | ||
{ "not", { | ||
x = leader.x, y = leader.y, terrain = 'K*,K*^*,*^K*', | ||
radius = 3, | ||
{ "filter_radius", { terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*' } } | ||
}}, -- That are not close and connected to a keep the leader is on | ||
{ "filter_adjacent_location", { | ||
terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*' | ||
}} -- That are not one-hex keeps | ||
} | ||
if #keeps < 1 then | ||
-- Skip if there aren't extra keeps to evaluate | ||
-- In this situation we'd only switch keeps if we were running away | ||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end | ||
return 0 | ||
end | ||
|
||
local enemy_leaders = get_reachable_enemy_leaders(leader) | ||
|
||
-- Look for the best keep | ||
local best_score, best_loc, best_turns = 0, {}, 3 | ||
for i,loc in ipairs(keeps) do | ||
-- Only consider keeps within 2 turns movement | ||
local path, cost = wesnoth.find_path(leader, loc[1], loc[2]) | ||
local score = 0 | ||
-- Prefer closer keeps to enemy | ||
local turns = math.ceil(cost/leader.max_moves) | ||
if turns <= 2 then | ||
score = 1/turns | ||
for j,e in ipairs(enemy_leaders) do | ||
score = score + 1 / M.distance_between(loc[1], loc[2], e.x, e.y) | ||
end | ||
|
||
if score > best_score then | ||
best_score = score | ||
best_loc = loc | ||
best_turns = turns | ||
end | ||
end | ||
end | ||
|
||
-- If we're on a keep, | ||
-- don't move to another keep unless it's much better when uncaptured villages are present | ||
if best_score > 0 and wesnoth.get_terrain_info(wesnoth.get_terrain(leader.x, leader.y)).keep then | ||
local close_unowned_village = (wesnoth.get_villages { | ||
{ "and", { | ||
x = leader.x, | ||
y = leader.y, | ||
radius = leader.max_moves | ||
}}, | ||
owner_side = 0 | ||
})[1] | ||
if close_unowned_village then | ||
local score = 1/best_turns | ||
for j,e in ipairs(enemy_leaders) do | ||
-- count all distances as three less than they actually are | ||
score = score + 1 / (M.distance_between(leader.x, leader.y, e.x, e.y) - 3) | ||
end | ||
|
||
if score > best_score then | ||
best_score = 0 | ||
end | ||
end | ||
end | ||
|
||
if best_score > 0 then | ||
local next_hop = AH.next_hop(leader, best_loc[1], best_loc[2]) | ||
|
||
if next_hop and ((next_hop[1] ~= leader.x) or (next_hop[2] ~= leader.y)) then | ||
-- See if there is a nearby village that can be captured without delaying progress | ||
local close_villages = wesnoth.get_villages( { | ||
{ "and", { x = next_hop[1], y = next_hop[2], radius = leader.max_moves }}, | ||
owner_side = 0 }) | ||
for i,loc in ipairs(close_villages) do | ||
local path_village, cost_village = wesnoth.find_path(leader, loc[1], loc[2]) | ||
if cost_village <= leader.moves then | ||
local dummy_leader = leader:clone() | ||
dummy_leader.x = loc[1] | ||
dummy_leader.y = loc[2] | ||
local path_keep, cost_keep = wesnoth.find_path(dummy_leader, best_loc[1], best_loc[2]) | ||
local turns_from_keep = math.ceil(cost_keep/leader.max_moves) | ||
if turns_from_keep < best_turns | ||
or (turns_from_keep == 1 and wesnoth.sides[wesnoth.current.side].gold < cheapest_unit_cost) | ||
then | ||
-- There is, go there instead | ||
next_hop = loc | ||
break | ||
end | ||
end | ||
end | ||
end | ||
|
||
data.leader_target = next_hop | ||
|
||
-- if we're on a keep, wait until there are no movable units on the castle before moving off | ||
data.leader_score = 290000 | ||
if wesnoth.get_terrain_info(wesnoth.get_terrain(leader.x, leader.y)).keep then | ||
local castle = wesnoth.get_locations { | ||
x = "1-"..width, y = "1-"..height, | ||
{ "and", { | ||
x = leader.x, y = leader.y, radius = 200, | ||
{ "filter_radius", { terrain = 'C*,K*,C*^*,K*^*,*^K*,*^C*' } } | ||
}} | ||
} | ||
local should_wait = false | ||
for i,loc in ipairs(castle) do | ||
local unit = wesnoth.get_unit(loc[1], loc[2]) | ||
if (not AH.is_visible_unit(wesnoth.current.side, unit)) then | ||
should_wait = false | ||
break | ||
elseif unit.moves > 0 then | ||
should_wait = true | ||
end | ||
end | ||
if should_wait then | ||
data.leader_score = 15000 | ||
end | ||
end | ||
|
||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end | ||
return data.leader_score | ||
end | ||
|
||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end | ||
return 0 | ||
end | ||
|
||
function ca_castle_switch:execution(cfg, data) | ||
local leader = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'yes' }[1] | ||
|
||
if AH.print_exec() then AH.print_ts(' Executing castle_switch CA') end | ||
if AH.show_messages() then wesnoth.wml_actions.message { speaker = leader.id, message = 'Switching castles' } end | ||
|
||
AH.checked_move(ai, leader, data.leader_target[1], data.leader_target[2]) | ||
data.leader_target = nil | ||
end | ||
|
||
return ca_castle_switch |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
------- Grab Villages CA -------------- | ||
|
||
local AH = wesnoth.require "ai/lua/ai_helper.lua" | ||
local BC = wesnoth.require "ai/lua/battle_calcs.lua" | ||
local M = wesnoth.map | ||
|
||
local ca_grab_villages = {} | ||
|
||
function ca_grab_villages:evaluation(cfg, data) | ||
local start_time, ca_name = wesnoth.get_time_stamp() / 1000., 'grab_villages' | ||
if AH.print_eval() then AH.print_ts(' - Evaluating grab_villages CA:') end | ||
|
||
-- Check if there are units with moves left | ||
local units = wesnoth.get_units { side = wesnoth.current.side, canrecruit = 'no', | ||
formula = 'movement_left > 0' | ||
} | ||
if (not units[1]) then | ||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end | ||
return 0 | ||
end | ||
|
||
local enemies = AH.get_attackable_enemies() | ||
|
||
local villages = wesnoth.get_villages() | ||
-- Just in case: | ||
if (not villages[1]) then | ||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end | ||
return 0 | ||
end | ||
|
||
-- First check if attacks are possible for any unit | ||
local return_value = 200000 | ||
-- If one with > 50% chance of kill is possible, set return_value to lower than combat CA | ||
local attacks = ai.get_attacks() | ||
for i,a in ipairs(attacks) do | ||
if (#a.movements == 1) and (a.chance_to_kill > 0.5) then | ||
return_value = 90000 | ||
break | ||
end | ||
end | ||
|
||
-- Also find which locations can be attacked by enemies | ||
local enemy_attack_map = BC.get_attack_map(enemies).units | ||
|
||
-- Now we go through the villages and units | ||
local max_rating, best_village, best_unit = - math.huge | ||
local village_ratings = {} | ||
for j,v in ipairs(villages) do | ||
-- First collect all information that only depends on the village | ||
local village_rating = 0 -- This is the unit independent rating | ||
|
||
local unit_in_way = wesnoth.get_unit(v[1], v[2]) | ||
|
||
-- If an enemy can get within one move of the village, we want to hold it | ||
if enemy_attack_map:get(v[1], v[2]) then | ||
village_rating = village_rating + 100 | ||
end | ||
|
||
-- Unowned and enemy-owned villages get a large bonus | ||
local owner = wesnoth.get_village_owner(v[1], v[2]) | ||
if (not owner) then | ||
village_rating = village_rating + 10000 | ||
else | ||
if wesnoth.is_enemy(owner, wesnoth.current.side) then village_rating = village_rating + 20000 end | ||
end | ||
|
||
local enemy_distance_from_village = AH.get_closest_enemy(v) | ||
|
||
-- Now we go on to the unit-dependent rating | ||
local best_unit_rating = - math.huge | ||
local reachable = false | ||
for i,u in ipairs(units) do | ||
-- Skip villages that have units other than 'u' itself on them | ||
local village_occupied = false | ||
if AH.is_visible_unit(wesnoth.current.side, unit_in_way) and ((unit_in_way ~= u)) then | ||
village_occupied = true | ||
end | ||
|
||
-- Rate all villages that can be reached and are unoccupied by other units | ||
if (not village_occupied) then | ||
-- Path finding is expensive, so we do a first cut simply by distance | ||
-- There is no way a unit can get to the village if the distance is greater than its moves | ||
local dist = M.distance_between(u.x, u.y, v[1], v[2]) | ||
if (dist <= u.moves) then | ||
local path, cost = wesnoth.find_path(u, v[1], v[2]) | ||
if (cost <= u.moves) then | ||
village_rating = village_rating - 1 | ||
reachable = true | ||
local rating = 0 | ||
|
||
-- Prefer strong units if enemies can reach the village, injured units otherwise | ||
if enemy_attack_map:get(v[1], v[2]) then | ||
rating = rating + u.hitpoints | ||
else | ||
rating = rating + u.max_hitpoints - u.hitpoints | ||
end | ||
|
||
-- Prefer not backtracking and moving more distant units to capture villages | ||
local enemy_distance_from_unit = AH.get_closest_enemy({u.x, u.y}) | ||
rating = rating - (enemy_distance_from_village + enemy_distance_from_unit)/5 | ||
|
||
if (rating > best_unit_rating) then | ||
best_unit_rating, best_unit = rating, u | ||
end | ||
end | ||
end | ||
end | ||
end | ||
village_ratings[v] = {village_rating, best_unit, reachable} | ||
end | ||
for j,v in ipairs(villages) do | ||
local rating = village_ratings[v][1] | ||
if village_ratings[v][3] and rating > max_rating then | ||
max_rating, best_village, best_unit = rating, v, village_ratings[v][2] | ||
end | ||
end | ||
|
||
if best_village then | ||
data.unit, data.village = best_unit, best_village | ||
if (max_rating >= 1000) then | ||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end | ||
return return_value | ||
else | ||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end | ||
return 0 | ||
end | ||
end | ||
if AH.print_eval() then AH.done_eval_messages(start_time, ca_name) end | ||
return 0 | ||
end | ||
|
||
function ca_grab_villages:execution(cfg, data) | ||
if AH.print_exec() then AH.print_ts(' Executing grab_villages CA') end | ||
if AH.show_messages() then wesnoth.wml_actions.message { speaker = data.unit.id, message = 'Grab villages' } end | ||
|
||
AH.movefull_stopunit(ai, data.unit, data.village) | ||
data.unit, data.village = nil, nil | ||
end | ||
|
||
return ca_grab_villages |
Oops, something went wrong.