wesnoth/src/ai.cpp
Mark de Wever 65c891c5b4 Fixed a crash when the attacker dies while performing a desparate attack.
The crash seemed only to happen under Windows but valgrind also
complained (bug #10496)
2007-12-15 14:46:06 +00:00

2343 lines
72 KiB
C++

/* $Id$ */
/*
Copyright (C) 2003 - 2007 by David White <dave@whitevine.net>
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version
or at your option any later version2
or at your option any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY.
See the COPYING file for more details.
*/
//! @file ai.cpp
//! Artificial intelligence - The computer commands the enemy.
#include "ai.hpp"
#include "ai2.hpp"
#include "ai_dfool.hpp"
#ifdef HAVE_PYTHON
#include "ai_python.hpp"
#endif
#include "actions.hpp"
#include "dialogs.hpp"
#include "game_config.hpp"
#include "game_events.hpp"
#include "game_preferences.hpp"
#include "gettext.hpp"
#include "menu_events.hpp"
#include "replay.hpp"
#include "statistics.hpp"
#include "unit_display.hpp"
#include "playturn.hpp"
#include "wml_exception.hpp"
#include <cassert>
#define LOG_AI LOG_STREAM(info, ai)
#define WRN_AI LOG_STREAM(warn, ai)
#define ERR_AI LOG_STREAM(err, ai)
//! A trivial ai that sits around doing absolutely nothing.
class idle_ai : public ai_interface {
public:
idle_ai(info& i) : ai_interface(i) {}
void play_turn() {
game_events::fire("ai turn");
}
};
//! Sample ai, with simple strategy.
class sample_ai : public ai_interface {
public:
sample_ai(info& i) : ai_interface(i) {}
void play_turn() {
game_events::fire("ai turn");
do_attacks();
get_villages();
do_moves();
do_recruitment();
}
protected:
void do_attacks() {
std::map<location,paths> possible_moves;
move_map srcdst, dstsrc;
calculate_possible_moves(possible_moves,srcdst,dstsrc,false);
for(unit_map::const_iterator i = get_info().units.begin(); i != get_info().units.end(); ++i) {
if(current_team().is_enemy(i->second.side())) {
location adjacent_tiles[6];
get_adjacent_tiles(i->first,adjacent_tiles);
int best_defense = -1;
std::pair<location,location> best_movement;
for(size_t n = 0; n != 6; ++n) {
typedef move_map::const_iterator Itor;
std::pair<Itor,Itor> range = dstsrc.equal_range(adjacent_tiles[n]);
while(range.first != range.second) {
const location& dst = range.first->first;
const location& src = range.first->second;
const unit_map::const_iterator un = get_info().units.find(src);
const t_translation::t_letter terrain = get_info().map.get_terrain(dst);
const int chance_to_hit = un->second.defense_modifier(terrain);
if(best_defense == -1 || chance_to_hit < best_defense) {
best_defense = chance_to_hit;
best_movement = *range.first;
}
++range.first;
}
}
if(best_defense != -1) {
move_unit(best_movement.second,best_movement.first,possible_moves);
battle_context bc(get_info().map, get_info().teams,
get_info().units, get_info().state,
get_info().gameinfo, best_movement.first,
i->first, -1, -1, current_team().aggression());
attack_enemy(best_movement.first,i->first,
bc.get_attacker_stats().attack_num,
bc.get_defender_stats().attack_num);
do_attacks();
return;
}
}
}
}
void get_villages() {
std::map<location,paths> possible_moves;
move_map srcdst, dstsrc;
calculate_possible_moves(possible_moves,srcdst,dstsrc,false);
for(move_map::const_iterator i = dstsrc.begin(); i != dstsrc.end(); ++i) {
if(get_info().map.is_village(i->first) && current_team().owns_village(i->first) == false) {
move_unit(i->second,i->first,possible_moves);
get_villages();
return;
}
}
}
void do_moves() {
unit_map::const_iterator leader;
for(leader = get_info().units.begin(); leader != get_info().units.end(); ++leader) {
if(leader->second.can_recruit() && current_team().is_enemy(leader->second.side())) {
break;
}
}
if(leader == get_info().units.end())
return;
std::map<location,paths> possible_moves;
move_map srcdst, dstsrc;
calculate_possible_moves(possible_moves,srcdst,dstsrc,false);
int closest_distance = -1;
std::pair<location,location> closest_move;
for(move_map::const_iterator i = dstsrc.begin(); i != dstsrc.end(); ++i) {
const int distance = distance_between(i->first,leader->first);
if(closest_distance == -1 || distance < closest_distance) {
closest_distance = distance;
closest_move = *i;
}
}
if(closest_distance != -1) {
move_unit(closest_move.second,closest_move.first,possible_moves);
do_moves();
}
}
void do_recruitment() {
const std::set<std::string>& options = current_team().recruits();
if (!options.empty()) {
const int choice = (rand()%options.size());
std::set<std::string>::const_iterator i = options.begin();
std::advance(i,choice);
const bool res = recruit(*i);
if(res) {
do_recruitment();
}
}
}
};
std::vector<std::string> get_available_ais()
{
std::vector<std::string> ais;
ais.push_back("default");
ais.push_back("sample_ai");
//ais.push_back("idle_ai");
ais.push_back("dfool_ai");
#ifdef HAVE_PYTHON
std::vector<std::string> scripts = python_ai::get_available_scripts();
ais.insert(ais.end(), scripts.begin(), scripts.end());
#endif
return ais;
}
ai_interface* create_ai(const std::string& name, ai_interface::info& info)
{
// To add an AI of your own, put
// if(name == "my_ai")
// return new my_ai(info);
// at the top of this function
if(name == "sample_ai")
return new sample_ai(info);
else if(name == "idle_ai")
return new idle_ai(info);
else if(name == "dfool_ai")
return new dfool::dfool_ai(info);
//else if(name == "advanced_ai")
// return new advanced_ai(info);
else if(name == "ai2")
return new ai2(info);
else if(name == "python_ai")
#ifdef HAVE_PYTHON
return new python_ai(info);
#else
{
LOG_STREAM(err, ai) << "No Python AI support available in this Wesnoth build!\n";
return new ai2(info);
}
#endif
else if(name != "")
LOG_STREAM(err, ai) << "AI not found: '" << name << "'\n";
return new ai(info);
}
ai::ai(ai_interface::info& info)
: ai_interface(info), threats_found_(false), disp_(info.disp),
map_(info.map), gameinfo_(info.gameinfo), units_(info.units),
teams_(info.teams), team_num_(info.team_num),
state_(info.state), consider_combat_(true), attack_depth_(0)
{}
bool ai::recruit_usage(const std::string& usage)
{
raise_user_interact();
const int min_gold = 0;
log_scope2(ai, "recruiting troops");
LOG_AI << "recruiting '" << usage << "'\n";
std::vector<std::string> options;
bool found = false;
// Find an available unit that can be recruited,
// matches the desired usage type, and comes in under budget.
const std::set<std::string>& recruits = current_team().recruits();
for(std::map<std::string,unit_type>::const_iterator i =
gameinfo_.unit_types.begin(); i != gameinfo_.unit_types.end(); ++i)
{
const std::string& name = i->second.id();
// If usage is empty consider any unit.
if (i->second.usage() == usage || usage == "") {
found = true;
if(recruits.count(name)
&& current_team().gold() - i->second.cost() > min_gold
&& not_recommended_units_.count(name) == 0)
{
LOG_AI << "recommending '" << name << "'\n";
options.push_back(name);
}
}
}
// From the available options, choose one at random
if(options.empty() == false) {
const int option = rand()%options.size();
return recruit(options[option]);
}
if (found) {
LOG_AI << "No available units to recruit that come under the price.\n";
} else {
WRN_AI << "Trying to recruit a: " << usage
<< " but no unit of that type (usage=) is available.\n";
}
return false;
}
bool ai_interface::recruit(const std::string& unit_name, location loc)
{
const std::set<std::string>& recruits = current_team().recruits();
const std::set<std::string>::const_iterator i = recruits.find(unit_name);
if(i == recruits.end()) {
return false;
}
const int num = std::distance(recruits.begin(),i);
// We have to add the recruit command now, because when the unit
// is created it has to have the recruit command in the recorder
// to be able to put random numbers into to generate unit traits.
// However, we're not sure if the transaction will be successful,
// so use a replay_undo object to cancel it if we don't get
// a confirmation for the transaction.
recorder.add_recruit(num,loc);
replay_undo replay_guard(recorder);
game_data::unit_type_map::const_iterator u = info_.gameinfo.unit_types.find(unit_name);
if(u == info_.gameinfo.unit_types.end()) {
return false;
}
// Check if we have enough money
if(current_team().gold() < u->second.cost()) {
return false;
}
LOG_AI << "trying recruit: team=" << (info_.team_num) <<
" type=" << unit_name <<
" cost=" << u->second.cost() <<
" loc=(" << loc << ')' <<
" gold=" << (current_team().gold()) <<
" (-> " << (current_team().gold()-u->second.cost()) << ")\n";
unit new_unit(&info_.gameinfo,&info_.units,&info_.map,&info_.state,&info_.teams,&u->second,info_.team_num,true);
// See if we can actually recruit (i.e. have enough room etc.)
std::string recruit_err = recruit_unit(info_.map,info_.team_num,info_.units,new_unit,loc,preferences::show_ai_moves());
if(recruit_err.empty()) {
statistics::recruit_unit(new_unit);
current_team().spend_gold(u->second.cost());
// Confirm the transaction - i.e. don't undo recruitment
replay_guard.confirm_transaction();
raise_unit_recruited();
const team_data data = calculate_team_data(current_team(),info_.team_num,info_.units);
LOG_AI <<
"recruit confirmed: team=" << (info_.team_num) <<
" units=" << data.units <<
" gold=" << data.gold <<
((data.net_income < 0) ? "" : "+") <<
data.net_income << "\n";
recorder.add_checksum_check(loc);
return true;
} else {
const team_data data = calculate_team_data(current_team(),info_.team_num,info_.units);
LOG_AI << recruit_err << "\n";
LOG_AI <<
"recruit UNconfirmed: team=" << (info_.team_num) <<
" units=" << data.units <<
" gold=" << data.gold <<
((data.net_income < 0) ? "" : "+") <<
data.net_income << "\n";
return false;
}
}
void ai_interface::raise_user_interact()
{
const int interact_time = 30;
const int time_since_interact = SDL_GetTicks() - last_interact_;
if(time_since_interact < interact_time) {
return;
}
user_interact_.notify_observers();
last_interact_ = SDL_GetTicks();
}
void ai_interface::diagnostic(const std::string& msg)
{
if(game_config::debug) {
info_.disp.set_diagnostic(msg);
}
}
void ai_interface::log_message(const std::string& msg)
{
if(game_config::debug) {
info_.disp.add_chat_message("ai",info_.team_num,msg,game_display::MESSAGE_PUBLIC,false);
}
}
gamemap::location ai_interface::move_unit(location from, location to,
std::map<location,paths>& possible_moves)
{
const location loc = move_unit_partial(from,to,possible_moves);
const unit_map::iterator u = info_.units.find(loc);
if(u != info_.units.end()) {
if(u->second.movement_left()==u->second.total_movement()) {
u->second.set_movement(0);
u->second.set_state("not_moved","yes");
} else {
u->second.set_movement(0);
}
}
return loc;
}
gamemap::location ai_interface::move_unit_partial(location from, location to,
std::map<location,paths>& possible_moves)
{
LOG_AI << "ai_interface::move_unit " << from << " -> " << to << '\n';
// Stop the user from issuing any commands while the unit is moving.
const events::command_disabler disable_commands;
info_.disp.select_hex(from);
info_.disp.update_display();
log_scope2(ai, "move_unit");
unit_map::iterator u_it = info_.units.find(from);
if(u_it == info_.units.end()) {
LOG_STREAM(err, ai) << "Could not find unit at " << from << '\n';
assert(false);
return location();
}
if(from == to) {
LOG_AI << "moving unit at " << from << " on spot. resetting moves\n";
return to;
}
const bool show_move = preferences::show_ai_moves();
const bool teleport = u_it->second.get_ability_bool("teleport",u_it->first);
paths current_paths(info_.map,info_.state,info_.gameinfo,info_.units,from,
info_.teams,false,teleport,current_team());
const std::map<location,paths>::iterator p_it = possible_moves.find(from);
if(p_it != possible_moves.end()) {
paths& p = p_it->second;
std::map<location,paths::route>::iterator rt = p.routes.begin();
for(; rt != p.routes.end(); ++rt) {
if(rt->first == to) {
break;
}
}
if(rt != p.routes.end()) {
u_it->second.set_movement(rt->second.move_left);
std::vector<location> steps = rt->second.steps;
while(steps.empty() == false && (!(info_.units.find(to) == info_.units.end() || from == to))){
LOG_AI << "AI attempting illegal move. Attempting to move onto existing unit\n";
LOG_AI << "\t" << info_.units.find(to)->second.underlying_description() <<" already on " << to << "\n";
LOG_AI <<"\tremoving "<<*(steps.end()-1)<<"\n";
to = *(steps.end()-1);
steps.pop_back();
LOG_AI << "\tresetting to " << from << " -> " << to << '\n';
}
if(steps.size()>1) { // First step is starting hex
unit_map::const_iterator utest=info_.units.find(*(steps.begin()+1));
if(utest != info_.units.end() && current_team().is_enemy(utest->second.side())){
LOG_STREAM(err, ai) << "AI tried to move onto existing enemy unit at"<<*(steps.begin())<<"\n";
// return(from);
}
// Check if there are any invisible units that we uncover
for(std::vector<location>::iterator i = steps.begin()+1; i != steps.end(); ++i) {
location adj[6];
get_adjacent_tiles(*i,adj);
size_t n;
for(n = 0; n != 6; ++n) {
// See if there is an enemy unit next to this tile.
// If it's invisible, we need to stop: we're ambushed.
// If it's not, we must be a skirmisher, otherwise AI wouldn't try.
// Or would it? If it doesn't cheat, it might...
const unit_map::const_iterator u = info_.units.find(adj[n]);
if (u != info_.units.end() && u->second.emits_zoc()
&& current_team().is_enemy(u->second.side())) {
if (u->second.invisible(adj[n], info_.units, info_.teams)) {
to = *i;
steps.erase(i,steps.end());
break;
} else {
if (!u_it->second.get_ability_bool("skirmisher",*i)){
LOG_STREAM(err, ai) << "AI tried to skirmish with non-skirmisher\n";
LOG_AI << "\tresetting destination from " <<to;
to = *i;
LOG_AI << " to " << to;
steps.erase(i,steps.end());
while(steps.empty() == false && (!(info_.units.find(to) == info_.units.end() || from == to))){
to = *(steps.end()-1);
steps.pop_back();
LOG_AI << "\tresetting to " << from << " -> " << to << '\n';
}
break;
}
}
}
}
if(n != 6) {
u_it->second.set_movement(0); // Enter enemy ZoC, no movement left
break;
}
}
}
steps.push_back(to); // Add the destination to the steps
if(show_move && unit_display::unit_visible_on_path(steps,
u_it->second, info_.units,info_.teams)) {
info_.disp.scroll_to_tiles(from,to);
unit_map::iterator up = info_.units.find(u_it->first);
unit_display::move_unit(info_.map,steps,up->second,info_.teams);
}
}
}
std::pair<gamemap::location,unit> *p = info_.units.extract(u_it->first);
p->first = to;
info_.units.add(p);
p->second.set_standing(info_.disp,p->first);
if(info_.map.is_village(to)) {
// If a new village is captured, disallow any future movement.
if (!info_.teams[info_.team_num-1].owns_village(to))
info_.units.find(to)->second.set_movement(-1);
get_village(to,info_.disp,info_.teams,info_.team_num-1,info_.units);
}
if(show_move) {
info_.disp.invalidate(to);
info_.disp.draw();
}
recorder.add_movement(from,to);
game_events::fire("moveto",to);
if((info_.teams.front().uses_fog() || info_.teams.front().uses_shroud()) &&
!info_.teams.front().fogged(to)) {
game_events::fire("sighted",to);
}
info_.disp.unhighlight_reach();
raise_unit_moved();
return to;
}
bool ai::multistep_move_possible(location from, location to, location via,
std::map<location,paths>& possible_moves)
{
const unit_map::const_iterator i = units_.find(from);
if(i != units_.end()) {
if(from != via && to != via && units_.count(via) == 0) {
LOG_AI << "when seeing if leader can move from "
<< from << " -> " << to
<< " seeing if can detour to keep at " << via << '\n';
const std::map<location,paths>::const_iterator moves = possible_moves.find(from);
if(moves != possible_moves.end()) {
LOG_AI << "found leader moves..\n";
int move_left = 0;
// See if the unit can make it to 'via', and if it can,
// how much movement it will have left when it gets there.
const paths::routes_map::const_iterator itor = moves->second.routes.find(via);
if(itor != moves->second.routes.end()) {
move_left = itor->second.move_left;
LOG_AI << "can make it to keep with " << move_left << " movement left\n";
unit temp_unit(i->second);
temp_unit.set_movement(move_left);
const temporary_unit_placer unit_placer(units_,via,temp_unit);
const paths unit_paths(map_,state_,gameinfo_,units_,via,teams_,false,false,current_team());
LOG_AI << "found " << unit_paths.routes.size() << " moves for temp leader\n";
// See if this leader could make it back to the keep.
if(unit_paths.routes.count(to) != 0) {
LOG_AI << "can make it back to the keep\n";
return true;
}
}
}
}
}
return false;
}
gamemap::location ai::move_unit(location from, location to, std::map<location,paths>& possible_moves)
{
std::map<location,paths> temp_possible_moves;
std::map<location,paths>* possible_moves_ptr = &possible_moves;
const unit_map::const_iterator i = units_.find(from);
if(i != units_.end() && i->second.can_recruit()) {
// If the leader isn't on its keep, and we can move to the keep
// and still make our planned movement, then try doing that.
const gamemap::location& start_pos = nearest_keep(i->first);
// If we can make it back to the keep and then to our original destination, do so.
if(multistep_move_possible(from,to,start_pos,possible_moves)) {
from = ai_interface::move_unit(from,start_pos,possible_moves);
if(from != start_pos) {
return from;
}
const unit_map::iterator itor = units_.find(from);
if(itor != units_.end()) {
// Just set the movement to one less than the maximum possible, since we know
// we can reach the destination, and we're going to move there immediately.
itor->second.set_movement(itor->second.total_movement()-1);
}
move_map srcdst, dstsrc;
calculate_possible_moves(temp_possible_moves,srcdst,dstsrc,false);
possible_moves_ptr = &temp_possible_moves;
}
do_recruitment();
}
if(units_.count(to) == 0 || from == to) {
const location res = ai_interface::move_unit(from,to,*possible_moves_ptr);
if(res != to) {
// We've been ambushed; find the ambushing unit and attack them.
adjacent_tiles_array locs;
get_adjacent_tiles(res,locs.data());
for(adjacent_tiles_array::const_iterator adj_i = locs.begin(); adj_i != locs.end(); ++adj_i) {
const unit_map::const_iterator itor = units_.find(*adj_i);
if(itor != units_.end() && current_team().is_enemy(itor->second.side()) &&
!itor->second.incapacitated()) {
battle_context bc(map_, teams_, units_, state_,
gameinfo_, res, *adj_i, -1, -1, current_team().aggression());
attack_enemy(res,itor->first,bc.get_attacker_stats().attack_num,bc.get_defender_stats().attack_num);
break;
}
}
}
return res;
} else {
return from;
}
}
bool ai::attack_close(const gamemap::location& loc) const
{
for(std::set<location>::const_iterator i = attacks_.begin(); i != attacks_.end(); ++i) {
if(distance_between(*i,loc) < 4) {
return true;
}
}
return false;
}
void ai::attack_enemy(const location& attacking_unit, const location& target,
int att_weapon, int def_weapon)
{
attacks_.insert(attacking_unit);
ai_interface::attack_enemy(attacking_unit,target,att_weapon,def_weapon);
}
void ai_interface::calculate_possible_moves(std::map<location,paths>& res, move_map& srcdst,
move_map& dstsrc, bool enemy, bool assume_full_movement,
const std::set<gamemap::location>* remove_destinations)
{
calculate_moves(info_.units,res,srcdst,dstsrc,enemy,assume_full_movement,remove_destinations);
}
void ai_interface::calculate_moves(const unit_map& units, std::map<location,paths>& res, move_map& srcdst,
move_map& dstsrc, bool enemy, bool assume_full_movement,
const std::set<gamemap::location>* remove_destinations,
bool see_all
)
{
for(unit_map::const_iterator un_it = units.begin(); un_it != units.end(); ++un_it) {
// If we are looking for the movement of enemies, then this unit must be an enemy unit.
// If we are looking for movement of our own units, it must be on our side.
// If we are assuming full movement, then it may be a unit on our side, or allied.
if(enemy && current_team().is_enemy(un_it->second.side()) == false ||
!enemy && !assume_full_movement && un_it->second.side() != info_.team_num ||
!enemy && assume_full_movement && current_team().is_enemy(un_it->second.side())) {
continue;
}
// Discount incapacitated units
if(un_it->second.incapacitated()) {
continue;
}
// We can't see where invisible enemy units might move.
if(enemy && un_it->second.invisible(un_it->first,units,info_.teams) && !see_all) {
continue;
}
// If it's an enemy unit, reset its moves while we do the calculations.
unit* held_unit = const_cast<unit*>(&(un_it->second));
const unit_movement_resetter move_resetter(*held_unit,enemy || assume_full_movement);
// Insert the trivial moves of staying on the same location.
if(un_it->second.movement_left() == un_it->second.total_movement()) {
std::pair<location,location> trivial_mv(un_it->first,un_it->first);
srcdst.insert(trivial_mv);
dstsrc.insert(trivial_mv);
}
const bool teleports = un_it->second.get_ability_bool("teleport",un_it->first);
res.insert(std::pair<gamemap::location,paths>(
un_it->first,paths(info_.map,info_.state,info_.gameinfo,units,
un_it->first,info_.teams,false,teleports,
current_team(),0,see_all)));
}
for(std::map<location,paths>::iterator m = res.begin(); m != res.end(); ++m) {
for(paths::routes_map::iterator rtit =
m->second.routes.begin(); rtit != m->second.routes.end(); ++rtit) {
const location& src = m->first;
const location& dst = rtit->first;
if(remove_destinations != NULL && remove_destinations->count(dst) != 0) {
continue;
}
bool friend_owns = false;
// Don't take friendly villages
if(!enemy && info_.map.is_village(dst)) {
for(size_t n = 0; n != info_.teams.size(); ++n) {
if(info_.teams[n].owns_village(dst)) {
if(n+1 != info_.team_num && current_team().is_enemy(n+1) == false) {
friend_owns = true;
}
break;
}
}
}
if(friend_owns) {
continue;
}
if(src != dst && units.find(dst) == units.end()) {
srcdst.insert(std::pair<location,location>(src,dst));
dstsrc.insert(std::pair<location,location>(dst,src));
}
}
}
}
void ai::remove_unit_from_moves(const gamemap::location& loc, move_map& srcdst, move_map& dstsrc)
{
srcdst.erase(loc);
for(move_map::iterator i = dstsrc.begin(); i != dstsrc.end(); ) {
if(i->second == loc) {
dstsrc.erase(i++);
} else {
++i;
}
}
}
namespace {
//! A structure for storing an item we're trying to protect.
struct protected_item {
protected_item(double value, int radius, const gamemap::location& loc) :
value(value), radius(radius), loc(loc) {}
double value;
int radius;
gamemap::location loc;
};
}
void ai::find_threats()
{
if(threats_found_) {
return;
}
threats_found_ = true;
const config& parms = current_team().ai_parameters();
std::vector<protected_item> items;
// We want to protect our leader.
const unit_map::const_iterator leader = find_leader(units_,team_num_);
if(leader != units_.end()) {
items.push_back(protected_item(
lexical_cast_default<double>(parms["protect_leader"], 1.0),
lexical_cast_default<int>(parms["protect_leader_radius"], 20),
leader->first));
}
// Look for directions to protect a specific location.
const config::child_list& locations = parms.get_children("protect_location");
for(config::child_list::const_iterator i = locations.begin(); i != locations.end(); ++i) {
items.push_back(protected_item(
lexical_cast_default<double>((**i)["value"], 1.0),
lexical_cast_default<int>((**i)["radius"], 20),
gamemap::location(**i, &get_info().game_state_)));
}
// Look for directions to protect a unit.
const config::child_list& protected_units = parms.get_children("protect_unit");
for(config::child_list::const_iterator j = protected_units.begin(); j != protected_units.end(); ++j) {
for(unit_map::const_iterator u = units_.begin(); u != units_.end(); ++u) {
if(game_events::unit_matches_filter(u, *j)) {
items.push_back(protected_item(
lexical_cast_default<double>((**j)["value"], 1.0),
lexical_cast_default<int>((**j)["radius"], 20),
u->first));
}
}
}
// Iterate over all protected locations, and if enemy units
// are within the protection radius, set them as hostile targets.
for(std::vector<protected_item>::const_iterator k = items.begin(); k != items.end(); ++k) {
const protected_item& item = *k;
for(unit_map::const_iterator u = units_.begin(); u != units_.end(); ++u) {
const int distance = distance_between(u->first,item.loc);
if(current_team().is_enemy(u->second.side()) && distance < item.radius) {
add_target(target(u->first, item.value * double(item.radius-distance) /
double(item.radius),target::THREAT));
}
}
}
}
void ai::play_turn()
{
// Protect against a memory over commitment:
//! @todo Not in the mood to figure out the exact cause:
// For some reason -1 hitpoints cause a segmentation fault.
// If -1 hitpoints are sent, we crash :/
try {
consider_combat_ = true;
game_events::fire("ai turn");
do_move();
} catch(std::bad_alloc) {
lg::wml_error << "Memory exhausted - a unit has either a lot of hitpoints or a negative amount.\n";
}
}
void ai::do_move()
{
log_scope2(ai, "doing ai move");
invalidate_defensive_position_cache();
raise_user_interact();
typedef paths::route route;
typedef std::map<location,paths> moves_map;
moves_map possible_moves, enemy_possible_moves;
move_map srcdst, dstsrc, enemy_srcdst, enemy_dstsrc;
calculate_possible_moves(possible_moves,srcdst,dstsrc,false,false,&avoided_locations());
calculate_possible_moves(enemy_possible_moves,enemy_srcdst,enemy_dstsrc,true);
const bool passive_leader = utils::string_bool(current_team().ai_parameters()["passive_leader"]);
if (passive_leader) {
unit_map::iterator leader = find_leader(units_,team_num_);
if(leader != units_.end()) {
remove_unit_from_moves(leader->first,srcdst,dstsrc);
}
}
// Execute goto-movements - first collect gotos in a list
std::vector<gamemap::location> gotos;
for(unit_map::iterator ui = units_.begin(); ui != units_.end(); ++ui) {
if(ui->second.get_goto() == ui->first) {
ui->second.set_goto(gamemap::location());
} else if(ui->second.side() == team_num_ && map_.on_board(ui->second.get_goto())) {
gotos.push_back(ui->first);
}
}
for(std::vector<gamemap::location>::const_iterator g = gotos.begin(); g != gotos.end(); ++g) {
unit_map::const_iterator ui = units_.find(*g);
int closest_distance = -1;
std::pair<location,location> closest_move;
for(move_map::const_iterator i = dstsrc.begin(); i != dstsrc.end(); ++i) {
if(i->second != ui->first) {
continue;
}
const int distance = distance_between(i->first,ui->second.get_goto());
if(closest_distance == -1 || distance < closest_distance) {
closest_distance = distance;
closest_move = *i;
}
}
if(closest_distance != -1) {
move_unit(ui->first,closest_move.first,possible_moves);
}
}
std::vector<attack_analysis> analysis;
LOG_AI << "combat phase\n";
if(consider_combat_) {
LOG_AI << "combat...\n";
consider_combat_ = do_combat(possible_moves,srcdst,dstsrc,enemy_srcdst,enemy_dstsrc);
if(consider_combat_) {
do_move();
return;
}
}
move_leader_to_goals(enemy_dstsrc);
LOG_AI << "get villages phase\n";
// Iterator could be invalidated by combat analysis or move_leader_to_goals.
unit_map::iterator leader = find_leader(units_,team_num_);
LOG_AI << "villages...\n";
const bool got_village = get_villages(possible_moves,srcdst,dstsrc,enemy_srcdst,enemy_dstsrc,leader);
if(got_village) {
do_move();
return;
}
LOG_AI << "healing phase\n";
LOG_AI << "healing...\n";
const bool healed_unit = get_healing(possible_moves,srcdst,enemy_dstsrc);
if(healed_unit) {
do_move();
return;
}
LOG_AI << "retreat phase\n";
LOG_AI << "retreating...\n";
leader = find_leader(units_,team_num_);
const bool retreated_unit = retreat_units(possible_moves,srcdst,dstsrc,enemy_dstsrc,leader);
if(retreated_unit) {
do_move();
return;
}
if(leader != units_.end()) {
remove_unit_from_moves(leader->first,srcdst,dstsrc);
}
find_threats();
LOG_AI << "move/targetting phase\n";
const bool met_invisible_unit = move_to_targets(possible_moves,srcdst,dstsrc,enemy_dstsrc,leader);
if(met_invisible_unit) {
LOG_AI << "met_invisible_unit\n";
do_move();
return;
}
LOG_AI << "done move to targets\n";
LOG_AI << "leader/recruitment phase\n";
// Recruitment phase and leader movement phase.
if(leader != units_.end()) {
if(!passive_leader) {
move_leader_to_keep(enemy_dstsrc);
}
do_recruitment();
if(!passive_leader) {
move_leader_after_recruit(srcdst,dstsrc,enemy_dstsrc);
}
}
}
bool ai::do_combat(std::map<gamemap::location,paths>& possible_moves, const move_map& srcdst,
const move_map& dstsrc, const move_map& enemy_srcdst, const move_map& enemy_dstsrc)
{
int ticks = SDL_GetTicks();
std::vector<attack_analysis> analysis = analyze_targets(srcdst, dstsrc,
enemy_srcdst, enemy_dstsrc);
int time_taken = SDL_GetTicks() - ticks;
LOG_AI << "took " << time_taken << " ticks for " << analysis.size()
<< " positions. Analyzing...\n";
ticks = SDL_GetTicks();
const int max_sims = 50000;
int num_sims = analysis.empty() ? 0 : max_sims/analysis.size();
if(num_sims < 20)
num_sims = 20;
if(num_sims > 40)
num_sims = 40;
LOG_AI << "simulations: " << num_sims << "\n";
const int max_positions = 30000;
const int skip_num = analysis.size()/max_positions;
std::vector<attack_analysis>::iterator choice_it = analysis.end();
double choice_rating = -1000.0;
for(std::vector<attack_analysis>::iterator it = analysis.begin();
it != analysis.end(); ++it) {
if(skip_num > 0 && ((it - analysis.begin())%skip_num) && it->movements.size() > 1)
continue;
const double rating = it->rating(current_team().aggression(),*this);
LOG_AI << "attack option rated at " << rating << " ("
<< current_team().aggression() << ")\n";
if(rating > choice_rating) {
choice_it = it;
choice_rating = rating;
}
}
time_taken = SDL_GetTicks() - ticks;
LOG_AI << "analysis took " << time_taken << " ticks\n";
if(choice_rating > 0.0) {
location from = choice_it->movements[0].first;
location to = choice_it->movements[0].second;
location target_loc = choice_it->target;
// Never used:
// const unit_map::const_iterator tgt = units_.find(target_loc);
const location arrived_at = move_unit(from,to,possible_moves);
if(arrived_at != to || units_.find(to) == units_.end()) {
LOG_STREAM(warn, ai) << "unit moving to attack has ended up unexpectedly at "
<< arrived_at << " when moving to " << to << " moved from "
<< from << '\n';
return true;
}
// Recalc appropriate weapons here: AI uses approximations.
battle_context bc(map_, teams_, units_, state_,
gameinfo_, to, target_loc, -1, -1,
current_team().aggression());
attack_enemy(to, target_loc, bc.get_attacker_stats().attack_num,
bc.get_defender_stats().attack_num);
// If this is the only unit in the attack, and the target
// is still alive, then also summon reinforcements
if(choice_it->movements.size() == 1 && units_.count(target_loc)) {
add_target(target(target_loc,3.0,target::BATTLE_AID));
}
return true;
} else {
return false;
}
}
//! This function should be called to attack an enemy.
//!
//! @param u The location of the attacking unit. (Note this shouldn't
//! be a reference since attack::attack() can invalidate the
//! unit_map and references to the map are also invalid then.)
//! @param target The location of the target unit. This unit must be in
//! range of the attacking unit's weapon. (See note at param u.)
//! @param weapon The number of the weapon (0-based) which should be used
//! by the attacker. (It must be a valid weapon of the attacker.)
//! @param def_weapon The number of the weapon (0-based) which should be used
//! by the defender. (It must be a valid weapon of the defender.)
void ai_interface::attack_enemy(const location u,
const location target, int weapon, int def_weapon)
{
// Stop the user from issuing any commands while the unit is attacking
const events::command_disabler disable_commands;
if(info_.units.count(u) && info_.units.count(target)) {
if(info_.units.find(target)->second.incapacitated()) {
LOG_STREAM(err, ai) << "attempt to attack unit that is turned to stone\n";
return;
}
if(!info_.units.find(u)->second.attacks_left()) {
LOG_STREAM(err, ai) << "attempt to attack twice with the same unit\n";
return;
}
recorder.add_attack(u,target,weapon,def_weapon);
attack(info_.disp, info_.map, info_.teams, u, target, weapon, def_weapon,
info_.units, info_.state, info_.gameinfo);
dialogs::advance_unit(info_.gameinfo,info_.map,info_.units,u,info_.disp,true);
const unit_map::const_iterator defender = info_.units.find(target);
if(defender != info_.units.end()) {
const size_t defender_team = size_t(defender->second.side()) - 1;
if(defender_team < info_.teams.size()) {
dialogs::advance_unit(info_.gameinfo, info_.map, info_.units,
target, info_.disp, !info_.teams[defender_team].is_human());
}
}
check_victory(info_.units,info_.teams);
raise_enemy_attacked();
}
}
std::vector<std::pair<gamemap::location,gamemap::location> > ai::get_village_combinations(
std::map<gamemap::location,paths>& possible_moves, const move_map& srcdst,
const move_map& dstsrc, const move_map& enemy_srcdst,
const move_map& enemy_dstsrc, unit_map::const_iterator leader,
std::set<gamemap::location>& taken_villages,
std::set<gamemap::location>& moved_units,
const std::vector<std::pair<gamemap::location,gamemap::location> >& village_moves,
std::vector<std::pair<gamemap::location,
gamemap::location> >::const_iterator start_at)
{
int leader_distance_from_keep = -1;
std::vector<std::pair<location,location> > result;
for(std::vector<std::pair<location,location> >::const_iterator i = start_at;
i != village_moves.end(); ++i) {
if(taken_villages.count(i->first) || moved_units.count(i->second)) {
continue;
}
int distance = -1;
if(leader != units_.end() && leader->first == i->second) {
const location& start_pos = nearest_keep(leader->first);;
distance = distance_between(start_pos,i->first);
}
taken_villages.insert(i->first);
moved_units.insert(i->second);
std::vector<std::pair<location,location> > res = get_village_combinations(
possible_moves, srcdst, dstsrc, enemy_srcdst, enemy_dstsrc, leader,
taken_villages, moved_units, village_moves, i+1);
// The result is better if it results in getting more villages,
// or if it results in the same number of villages,
// but the leader ends closer to their keep.
const bool result_better = res.size() >= result.size() ||
res.size()+1 == result.size() &&
distance != -1 &&
distance < leader_distance_from_keep;
if(result_better) {
result.swap(res);
result.push_back(*i);
if(distance != -1) {
leader_distance_from_keep = distance;
}
}
taken_villages.erase(i->first);
moved_units.erase(i->second);
}
return result;
}
bool ai::get_villages(std::map<gamemap::location,paths>& possible_moves,
const move_map& srcdst, const move_map& dstsrc, const move_map& enemy_srcdst,
const move_map& enemy_dstsrc, unit_map::iterator &leader)
{
LOG_AI << "deciding which villages we want...\n";
location start_pos;
if(leader != units_.end()) {
start_pos = nearest_keep(leader->first);
}
std::map<location,double> vulnerability;
// We want to build up a list of possible moves we can make
// that will capture villages.
// Limit the moves to 'max_village_moves' to make sure
// things don't get out of hand.
const size_t max_village_moves = 50;
std::vector<std::pair<location,location> > village_moves;
for(move_map::const_iterator j = dstsrc.begin();
j != dstsrc.end() && village_moves.size() < max_village_moves; ++j) {
if(map_.is_village(j->first) == false) {
continue;
}
bool want_village = true, owned = false;
for(size_t n = 0; n != teams_.size(); ++n) {
owned = teams_[n].owns_village(j->first);
if(owned && !current_team().is_enemy(n+1)) {
want_village = false;
}
if(owned) {
break;
}
}
if(want_village == false) {
continue;
}
// If it is a neutral village, and we have no leader,
// then the village is of no use to us, and we don't want it.
if(!owned && leader == units_.end()) {
continue;
}
// If we have a decent amount of gold, and the leader can't access
// the keep this turn if they get this village,
// then don't get this village with them.
if(want_village &&
leader != units_.end() &&
current_team().gold() > 20 &&
leader->first == j->second &&
leader->first != start_pos &&
multistep_move_possible(j->second,j->first,start_pos,possible_moves) == false) {
continue;
}
double threat = 0.0;
const std::map<location,double>::const_iterator vuln = vulnerability.find(j->first);
if(vuln != vulnerability.end()) {
threat = vuln->second;
} else {
threat = power_projection(j->first,enemy_dstsrc);
vulnerability.insert(std::pair<location,double>(j->first,threat));
}
const unit_map::const_iterator u = units_.find(j->second);
if(u == units_.end() || utils::string_bool(u->second.get_state("guardian"))) {
continue;
}
const unit& un = u->second;
if(un.hitpoints() < (threat*2*un.defense_modifier(map_.get_terrain(j->first)))/100) {
continue;
}
village_moves.push_back(*j);
}
std::set<location> taken_villages, moved_units;
const int ticks = SDL_GetTicks();
LOG_AI << "get_villages()..." << village_moves.size() << "\n";
const std::vector<std::pair<location,location> >& moves = get_village_combinations(
possible_moves, srcdst, dstsrc, enemy_srcdst, enemy_dstsrc, leader,
taken_villages, moved_units, village_moves, village_moves.begin());
LOG_AI << "get_villages() done: " << (SDL_GetTicks() - ticks) << "\n";
// Move all the units to get villages, however move the leader last,
// so that the castle will be cleared if it wants to stop to recruit along the way.
std::pair<location,location> leader_move;
int moves_made = 0;
for(std::vector<std::pair<location,location> >::const_iterator i = moves.begin();
i != moves.end(); ++i) {
if(leader != units_.end() && leader->first == i->second) {
leader_move = *i;
} else {
if(units_.count(i->first) == 0) {
const location loc = move_unit(i->second,i->first,possible_moves);
++moves_made;
leader = find_leader(units_, team_num_);
// If we didn't make it to the destination, it means we were ambushed.
if(loc != i->first) {
return true;
}
const unit_map::const_iterator new_unit = units_.find(loc);
if(new_unit != units_.end() &&
power_projection(i->first,enemy_dstsrc) >= new_unit->second.hitpoints()/4) {
add_target(target(new_unit->first,1.0,target::SUPPORT));
}
}
}
}
if(leader_move.second.valid()) {
if(units_.count(leader_move.first) == 0) {
gamemap::location loc = move_unit(leader_move.second,leader_move.first,possible_moves);
++moves_made;
// Update leader iterator, since we moved it.
leader = units_.find(loc);
}
}
return moves_made > 0 && village_moves.size() == max_village_moves;
}
bool ai::get_healing(std::map<gamemap::location,paths>& possible_moves,
const move_map& srcdst, const move_map& enemy_dstsrc)
{
// Find units in need of healing.
unit_map::iterator u_it = units_.begin();
for(; u_it != units_.end(); ++u_it) {
unit& u = u_it->second;
// If the unit is on our side, has lost as many or more than
// 1/2 round worth of healing, and doesn't regenerate itself,
// then try to find a vacant village for it to rest in.
if(u.side() == team_num_ &&
u.max_hitpoints() - u.hitpoints() >= game_config::poison_amount/2 &&
!u.get_ability_bool("regenerate",u_it->first)) {
// Look for the village which is the least vulnerable to enemy attack.
typedef std::multimap<location,location>::const_iterator Itor;
std::pair<Itor,Itor> it = srcdst.equal_range(u_it->first);
double best_vulnerability = 100000.0;
Itor best_loc = it.second;
while(it.first != it.second) {
const location& dst = it.first->second;
if(map_.gives_healing(dst) && (units_.find(dst) == units_.end() || dst == u_it->first)) {
const double vuln = power_projection(it.first->first, enemy_dstsrc);
LOG_AI << "found village with vulnerability: " << vuln << "\n";
if(vuln < best_vulnerability || best_loc == it.second) {
best_vulnerability = vuln;
best_loc = it.first;
LOG_AI << "chose village " << dst << '\n';
}
}
++it.first;
}
// If we have found an eligible village:
if(best_loc != it.second) {
const location& src = best_loc->first;
const location& dst = best_loc->second;
LOG_AI << "moving unit to village for healing...\n";
move_unit(src,dst,possible_moves);
return true;
}
}
}
return false;
}
bool ai::should_retreat(const gamemap::location& loc, const unit_map::const_iterator un,
const move_map& srcdst, const move_map& dstsrc, const move_map& enemy_dstsrc,
double caution)
{
if(caution <= 0.0) {
return false;
}
const double optimal_terrain = best_defensive_position(un->first, dstsrc,
srcdst, enemy_dstsrc).chance_to_hit/100.0;
const double proposed_terrain =
un->second.defense_modifier(map_.get_terrain(loc))/100.0;
// The 'exposure' is the additional % chance to hit
// this unit receives from being on a sub-optimal defensive terrain.
const double exposure = proposed_terrain - optimal_terrain;
const double our_power = power_projection(loc,dstsrc);
const double their_power = power_projection(loc,enemy_dstsrc);
return caution*their_power*(1.0+exposure) > our_power;
}
bool ai::retreat_units(std::map<gamemap::location,paths>& possible_moves,
const move_map& srcdst, const move_map& dstsrc,
const move_map& enemy_dstsrc, unit_map::const_iterator leader)
{
// Get versions of the move map that assume that all units are at full movement
std::map<gamemap::location,paths> dummy_possible_moves;
move_map fullmove_srcdst;
move_map fullmove_dstsrc;
calculate_possible_moves(dummy_possible_moves, fullmove_srcdst, fullmove_dstsrc,
false, true, &avoided_locations());
gamemap::location leader_adj[6];
if(leader != units_.end()) {
get_adjacent_tiles(leader->first,leader_adj);
}
for(unit_map::iterator i = units_.begin(); i != units_.end(); ++i) {
if(i->second.side() == team_num_ &&
i->second.movement_left() == i->second.total_movement() &&
unit_map::const_iterator(i) != leader &&
!i->second.incapacitated()) {
// This unit still has movement left, and is a candidate to retreat.
// We see the amount of power of each side on the situation,
// and decide whether it should retreat.
if(should_retreat(i->first, i, fullmove_srcdst, fullmove_dstsrc,
enemy_dstsrc, current_team().caution())) {
bool can_reach_leader = false;
// Time to retreat. Look for the place where the power balance
// is most in our favor.
// If we can't find anywhere where we like the power balance,
// just try to get to the best defensive hex.
typedef move_map::const_iterator Itor;
std::pair<Itor,Itor> itors = srcdst.equal_range(i->first);
gamemap::location best_pos, best_defensive(i->first);
double best_rating = 0.0;
int best_defensive_rating = i->second.defense_modifier(map_.get_terrain(i->first))
- (map_.is_village(i->first) ? 10 : 0);
while(itors.first != itors.second) {
if(leader != units_.end() && std::count(leader_adj,
leader_adj + 6, itors.first->second)) {
can_reach_leader = true;
break;
}
// We rate the power balance of a hex based on our power projection
// compared to theirs, multiplying their power projection by their
// chance to hit us on the hex we're planning to flee to.
const gamemap::location& hex = itors.first->second;
const int defense = i->second.defense_modifier(map_.get_terrain(hex));
const double our_power = power_projection(hex,dstsrc);
const double their_power = power_projection(hex,enemy_dstsrc) * double(defense)/100.0;
const double rating = our_power - their_power;
if(rating > best_rating) {
best_pos = hex;
best_rating = rating;
}
// Give a bonus for getting to a village.
const int modified_defense = defense - (map_.is_village(hex) ? 10 : 0);
if(modified_defense < best_defensive_rating) {
best_defensive_rating = modified_defense;
best_defensive = hex;
}
++itors.first;
}
// If the unit is in range of its leader, it should
// never retreat -- it has to defend the leader instead.
if(can_reach_leader) {
continue;
}
if(!best_pos.valid()) {
best_pos = best_defensive;
}
// If we can't move, we should be more aggressive in lashing out.
if (best_pos == i->first) {
if (i->second.attacks_left()) {
return desperate_attack(i->first);
}
return false;
}
if(best_pos.valid()) {
LOG_AI << "retreating '" << i->second.id() << "' " << i->first
<< " -> " << best_pos << '\n';
move_unit(i->first,best_pos,possible_moves);
return true;
}
}
}
}
return false;
}
bool ai::move_to_targets(std::map<gamemap::location, paths>& possible_moves,
move_map& srcdst, move_map& dstsrc, const move_map& enemy_dstsrc,
unit_map::const_iterator leader)
{
LOG_AI << "finding targets...\n";
std::vector<target> targets;
for(;;) {
if(targets.empty()) {
targets = find_targets(leader,enemy_dstsrc);
targets.insert(targets.end(),additional_targets_.begin(),
additional_targets_.end());
if(targets.empty()) {
return false;
}
}
LOG_AI << "choosing move...\n";
std::pair<location,location> move = choose_move(targets, srcdst,
dstsrc, enemy_dstsrc);
for(std::vector<target>::const_iterator ittg = targets.begin();
ittg != targets.end(); ++ittg) {
assert(map_.on_board(ittg->loc));
}
if(move.first.valid() == false) {
break;
}
if(move.second.valid() == false) {
return true;
}
LOG_AI << "move: " << move.first << " -> " << move.second << '\n';
const location arrived_at = move_unit(move.first,move.second,possible_moves);
// We didn't arrive at our intended destination.
// We return true, meaning that the AI algorithm
// should be recalculated from the start.
if(arrived_at != move.second) {
LOG_STREAM(warn, ai) << "didn't arrive at destination\n";
return true;
}
const unit_map::const_iterator u_it = units_.find(arrived_at);
// Event could have done anything: check
if (u_it == units_.end() || u_it->second.incapacitated()) {
LOG_STREAM(warn, ai) << "stolen or incapacitated\n";
} else {
// Search to see if there are any enemy units next to the tile
// which really should be attacked now the move is done.
gamemap::location adj[6];
get_adjacent_tiles(arrived_at,adj);
gamemap::location target;
for(int n = 0; n != 6; ++n) {
const unit_map::iterator enemy = find_visible_unit(units_,adj[n],
map_,
teams_,current_team());
if(enemy != units_.end() &&
current_team().is_enemy(enemy->second.side()) && !enemy->second.incapacitated()) {
// Current behavior is to only make risk-free attacks.
battle_context bc(map_, teams_, units_, state_, gameinfo_, arrived_at, adj[n], -1, -1, 100.0);
if (bc.get_defender_stats().damage == 0) {
attack_enemy(arrived_at, adj[n], bc.get_attacker_stats().attack_num,
bc.get_defender_stats().attack_num);
break;
}
}
}
}
// Don't allow any other units to move onto the tile
// our unit just moved onto
typedef move_map::iterator Itor;
std::pair<Itor,Itor> del = dstsrc.equal_range(arrived_at);
dstsrc.erase(del.first,del.second);
}
return false;
}
int ai::average_resistance_against(const unit_type& a, const unit_type& b) const
{
int weighting_sum = 0, defense = 0;
const std::map<t_translation::t_letter, size_t>& terrain =
map_.get_weighted_terrain_frequencies();
for (std::map<t_translation::t_letter, size_t>::const_iterator j = terrain.begin(),
j_end = terrain.end(); j != j_end; ++j)
{
// Use only reachable tiles when computing the average defense.
if (a.movement_type().movement_cost(map_, j->first) < 99) {
defense += a.movement_type().defense_modifier(map_, j->first) * j->second;
weighting_sum += j->second;
}
}
if (weighting_sum == 0) {
// This unit can't move on this map, so just get the average weighted
// of all available terrains. This still is a kind of silly
// since the opponent probably can't recruit this unit and it's a static unit.
for (std::map<t_translation::t_letter, size_t>::const_iterator jj = terrain.begin(),
jj_end = terrain.end(); jj != jj_end; ++jj)
{
defense += a.movement_type().defense_modifier(map_, jj->first) * jj->second;
weighting_sum += jj->second;
}
}
if(weighting_sum != 0) {
defense /= weighting_sum;
} else {
ERR_AI << "The weighting sum is 0 and is ignored.\n";
}
LOG_AI << "average defense of '" << a.id() << "': " << defense << "\n";
int sum = 0, weight_sum = 0;
// calculation of the average damage taken
bool steadfast = a.has_ability_by_id("steadfast");
bool living = !a.not_living();
const std::vector<attack_type>& attacks = b.attacks();
for (std::vector<attack_type>::const_iterator i = attacks.begin(),
i_end = attacks.end(); i != i_end; ++i)
{
int resistance = a.movement_type().resistance_against(*i);
// Apply steadfast resistance modifier.
if (steadfast && resistance < 100)
resistance = maximum<int>(resistance * 2 - 100, 50);
// Do not look for filters or values, simply assume 70% if CTH is customized.
int cth = i->get_special_bool("chance_to_hit", true) ? 70 : defense;
int weight = i->damage() * i->num_attacks();
// if cth == 0 the division will do 0/0 so don't execute this part
if (living && cth != 0 && i->get_special_bool("poison", true)) {
// Compute the probability of not poisoning the unit.
int prob = 100;
for (int j = 0; j < i->num_attacks(); ++j)
prob = prob * (100 - cth);
// Assume poison works one turn.
weight += game_config::poison_amount * (100 - prob) / 100;
}
sum += cth * resistance * weight * weight; // average damage * weight
weight_sum += weight;
}
// normalize by HP
sum /= maximum<int>(1,minimum<int>(a.hitpoints(),1000)); // avoid values really out of range
// Catch division by zero here if the attacking unit
// has zero attacks and/or zero damage.
// If it has no attack at all, the ai shouldn't prefer
// that unit anyway.
if (weight_sum == 0) {
return sum;
}
return sum/weight_sum;
}
int ai::compare_unit_types(const unit_type& a, const unit_type& b) const
{
const int a_effectiveness_vs_b = average_resistance_against(b,a);
const int b_effectiveness_vs_a = average_resistance_against(a,b);
LOG_AI << "comparison of '" << a.id() << " vs " << b.id() << ": "
<< a_effectiveness_vs_b << " - " << b_effectiveness_vs_a << " = "
<< (a_effectiveness_vs_b - b_effectiveness_vs_a) << "\n";
return a_effectiveness_vs_b - b_effectiveness_vs_a;
}
void ai::analyze_potential_recruit_combat()
{
if(unit_combat_scores_.empty() == false ||
utils::string_bool(current_team().ai_parameters()["recruitment_ignore_bad_combat"])) {
return;
}
log_scope2(ai, "analyze_potential_recruit_combat()");
// Records the best combat analysis for each usage type.
std::map<std::string,int> best_usage;
const std::set<std::string>& recruits = current_team().recruits();
std::set<std::string>::const_iterator i;
for(i = recruits.begin(); i != recruits.end(); ++i) {
const game_data::unit_type_map::const_iterator info = gameinfo_.unit_types.find(*i);
if(info == gameinfo_.unit_types.end() || not_recommended_units_.count(*i)) {
continue;
}
int score = 0, weighting = 0;
for(unit_map::const_iterator j = units_.begin(); j != units_.end(); ++j) {
if(j->second.can_recruit() || current_team().is_enemy(j->second.side()) == false) {
continue;
}
unit const &un = j->second;
const game_data::unit_type_map::const_iterator enemy_info = gameinfo_.unit_types.find(un.id());
WML_ASSERT((enemy_info != gameinfo_.unit_types.end()), _("Unknown unit type : ") + un.id());
int weight = un.cost() * un.hitpoints() / un.max_hitpoints();
weighting += weight;
score += compare_unit_types(info->second, enemy_info->second) * weight;
}
if(weighting != 0) {
score /= weighting;
}
LOG_AI << "combat score of '" << *i << "': " << score << "\n";
unit_combat_scores_[*i] = score;
if(best_usage.count(info->second.usage()) == 0 ||
score > best_usage[info->second.usage()]) {
best_usage[info->second.usage()] = score;
}
}
// Recommend not to use units of a certain usage type
// if they have a score more than 600 below
// the best unit of that usage type.
for(i = recruits.begin(); i != recruits.end(); ++i) {
const game_data::unit_type_map::const_iterator info = gameinfo_.unit_types.find(*i);
if(info == gameinfo_.unit_types.end() || not_recommended_units_.count(*i)) {
continue;
}
if(unit_combat_scores_[*i] + 600 < best_usage[info->second.usage()]) {
LOG_AI << "recommending not to use '" << *i << "' because of poor combat performance "
<< unit_combat_scores_[*i] << "/" << best_usage[info->second.usage()] << "\n";
not_recommended_units_.insert(*i);
}
}
}
namespace {
struct target_comparer_distance {
target_comparer_distance(const gamemap::location& loc) : loc_(loc) {}
bool operator()(const ai::target& a, const ai::target& b) const {
return distance_between(a.loc,loc_) < distance_between(b.loc,loc_);
}
private:
gamemap::location loc_;
};
}
void ai::analyze_potential_recruit_movements()
{
if(unit_movement_scores_.empty() == false ||
utils::string_bool(current_team().ai_parameters()["recruitment_ignore_bad_movement"])) {
return;
}
const unit_map::const_iterator leader = find_leader(units_,team_num_);
if(leader == units_.end()) {
return;
}
const location& start = nearest_keep(leader->first);
if(map_.on_board(start) == false) {
return;
}
log_scope2(ai, "analyze_potential_recruit_movements()");
const unsigned int max_targets = 5;
const move_map srcdst, dstsrc;
std::vector<target> targets = find_targets(leader,dstsrc);
if(targets.size() > max_targets) {
std::sort(targets.begin(),targets.end(),target_comparer_distance(start));
targets.erase(targets.begin()+max_targets,targets.end());
}
const std::set<std::string>& recruits = current_team().recruits();
LOG_AI << "targets: " << targets.size() << "\n";
std::map<std::string,int> best_scores;
for(std::set<std::string>::const_iterator i = recruits.begin(); i != recruits.end(); ++i) {
const game_data::unit_type_map::const_iterator info = gameinfo_.unit_types.find(*i);
if(info == gameinfo_.unit_types.end()) {
continue;
}
const unit temp_unit(&get_info().gameinfo, &get_info().units,&get_info().map,
&get_info().state, &get_info().teams, &info->second, team_num_);
unit_map units;
const temporary_unit_placer placer(units,start,temp_unit);
int cost = 0;
int targets_reached = 0;
int targets_missed = 0;
const shortest_path_calculator calc(temp_unit,current_team(),units,teams_,map_);
for(std::vector<target>::const_iterator t = targets.begin(); t != targets.end(); ++t) {
LOG_AI << "analyzing '" << *i << "' getting to target...\n";
const paths::route& route = a_star_search(start, t->loc, 100.0, &calc,
get_info().map.w(), get_info().map.h());
if(route.steps.empty() == false) {
LOG_AI << "made it: " << route.move_left << "\n";
cost += route.move_left;
++targets_reached;
} else {
LOG_AI << "failed\n";
++targets_missed;
}
}
if(targets_reached == 0 || targets_missed >= targets_reached*2) {
unit_movement_scores_[*i] = 100000;
not_recommended_units_.insert(*i);
} else {
const int average_cost = cost/targets_reached;
const int score = (average_cost * (targets_reached+targets_missed))/targets_reached;
unit_movement_scores_[*i] = score;
const std::map<std::string,int>::const_iterator current_best = best_scores.find(temp_unit.usage());
if(current_best == best_scores.end() || score < current_best->second) {
best_scores[temp_unit.usage()] = score;
}
}
}
for(std::map<std::string,int>::iterator j = unit_movement_scores_.begin();
j != unit_movement_scores_.end(); ++j) {
const game_data::unit_type_map::const_iterator info =
gameinfo_.unit_types.find(j->first);
if(info == gameinfo_.unit_types.end()) {
continue;
}
const int best_score = best_scores[info->second.usage()];
if(best_score > 0) {
j->second = (j->second*10)/best_score;
if(j->second > 15) {
LOG_AI << "recommending against recruiting '" << j->first << "' (score: " << j->second << ")\n";
not_recommended_units_.insert(j->first);
} else {
LOG_AI << "recommending recruit of '" << j->first << "' (score: " << j->second << ")\n";
}
}
}
if(not_recommended_units_.size() == unit_movement_scores_.size()) {
not_recommended_units_.clear();
}
}
void ai::do_recruitment()
{
const unit_map::const_iterator leader = find_leader(units_,team_num_);
if(leader == units_.end()) {
return;
}
const location& start_pos = nearest_keep(leader->first);
analyze_potential_recruit_movements();
analyze_potential_recruit_combat();
size_t neutral_villages = 0;
// We recruit the initial allocation of scouts
// based on how many neutral villages there are
// that are closer to us than to other keeps.
const std::vector<location>& villages = map_.villages();
for(std::vector<location>::const_iterator v = villages.begin(); v != villages.end(); ++v) {
const int owner = village_owner(*v,teams_);
if(owner == -1) {
const size_t distance = distance_between(start_pos,*v);
bool closest = true;
for(std::vector<team>::const_iterator i = teams_.begin(); i != teams_.end(); ++i) {
const int index = i - teams_.begin() + 1;
const gamemap::location& loc = map_.starting_position(index);
if(loc != start_pos && distance_between(loc,*v) < distance) {
closest = false;
break;
}
}
if(closest) {
++neutral_villages;
}
}
}
// The villages per scout is for a two-side battle,
// accounting for all neutral villages on the map.
// We only look at villages closer to us, so we halve it,
// making us get twice as many scouts.
const int villages_per_scout = current_team().villages_per_scout()/2;
// Get scouts depending on how many neutral villages there are.
int scouts_wanted = villages_per_scout > 0 ? neutral_villages/villages_per_scout : 0;
LOG_AI << "scouts_wanted: " << neutral_villages << "/"
<< villages_per_scout << " = " << scouts_wanted << "\n";
std::map<std::string,int> unit_types;
for(unit_map::const_iterator u = units_.begin(); u != units_.end(); ++u) {
if(u->second.side() == team_num_) {
++unit_types[u->second.usage()];
}
}
LOG_AI << "we have " << unit_types["scout"] << " scouts already and we want "
<< scouts_wanted << " in total\n";
while(unit_types["scout"] < scouts_wanted) {
if(recruit_usage("scout") == false)
break;
++unit_types["scout"];
}
std::vector<std::string> options = current_team().recruitment_pattern();
// If there is no recruitment_pattern use "" which makes us consider
// any unit available.
if (options.empty()) {
options.push_back("");
}
// Buy units as long as we have room and can afford it.
while(recruit_usage(options[rand()%options.size()])) {
}
}
void ai::move_leader_to_goals( const move_map& enemy_dstsrc)
{
const config* const goal = current_team().ai_parameters().child("leader_goal");
if(goal == NULL) {
LOG_AI << "No goal found\n";
return;
}
const gamemap::location dst(*goal, &get_info().game_state_);
if (!dst.valid()) {
ERR_AI << "Invalid goal\n";
return;
}
const unit_map::iterator leader = find_leader(units_,team_num_);
if(leader == units_.end() || leader->second.incapacitated()) {
WRN_AI << "Leader not found\n";
return;
}
LOG_AI << "Doing recruitment before goals\n";
do_recruitment();
shortest_path_calculator calc(leader->second, current_team(), units_, teams_, map_);
const paths::route route = a_star_search(leader->first, dst, 1000.0, &calc,
get_info().map.w(), get_info().map.h());
if(route.steps.empty()) {
LOG_AI << "route empty";
return;
}
const paths leader_paths(map_, state_, gameinfo_, units_, leader->first,
teams_, false, false, current_team());
std::map<gamemap::location,paths> possible_moves;
possible_moves.insert(std::pair<gamemap::location,paths>(leader->first,leader_paths));
gamemap::location loc;
for(std::vector<gamemap::location>::const_iterator itor = route.steps.begin();
itor != route.steps.end(); ++itor) {
if(leader_paths.routes.count(*itor) == 1 &&
power_projection(*itor,enemy_dstsrc) < double(leader->second.hitpoints()/2)) {
loc = *itor;
}
}
if(loc.valid()) {
LOG_AI << "Moving leader to goal\n";
move_unit(leader->first,loc,possible_moves);
}
}
void ai::move_leader_to_keep(const move_map& enemy_dstsrc)
{
const unit_map::iterator leader = find_leader(units_,team_num_);
if(leader == units_.end() || leader->second.incapacitated()) {
return;
}
// Find where the leader can move
const paths leader_paths(map_, state_, gameinfo_, units_, leader->first,
teams_, false, false, current_team());
const gamemap::location& start_pos = nearest_keep(leader->first);
std::map<gamemap::location,paths> possible_moves;
possible_moves.insert(std::pair<gamemap::location,paths>(leader->first,leader_paths));
// If the leader is not on his starting location, move him there.
if(leader->first != start_pos) {
const paths::routes_map::const_iterator itor = leader_paths.routes.find(start_pos);
if(itor != leader_paths.routes.end() && units_.count(start_pos) == 0) {
move_unit(leader->first,start_pos,possible_moves);
} else {
// Make a map of the possible locations the leader can move to,
// ordered by the distance from the keep.
std::multimap<int,gamemap::location> moves_toward_keep;
// The leader can't move to his keep, try to move to the closest location
// to the keep where there are no enemies in range.
const int current_distance = distance_between(leader->first,start_pos);
for(paths::routes_map::const_iterator i = leader_paths.routes.begin();
i != leader_paths.routes.end(); ++i) {
const int new_distance = distance_between(i->first,start_pos);
if(new_distance < current_distance) {
moves_toward_keep.insert(std::pair<int,gamemap::location>(new_distance,i->first));
}
}
// Find the first location which we can move to,
// without the threat of enemies.
for(std::multimap<int,gamemap::location>::const_iterator j = moves_toward_keep.begin();
j != moves_toward_keep.end(); ++j) {
if(enemy_dstsrc.count(j->second) == 0) {
move_unit(leader->first,j->second,possible_moves);
break;
}
}
}
}
}
void ai::move_leader_after_recruit(const move_map& /*srcdst*/,
const move_map& /*dstsrc*/, const move_map& enemy_dstsrc)
{
LOG_AI << "moving leader after recruit...\n";
unit_map::iterator leader = find_leader(units_,team_num_);
if(leader == units_.end() || leader->second.incapacitated()) {
return;
}
const paths leader_paths(map_, state_, gameinfo_, units_, leader->first,
teams_, false, false, current_team());
std::map<gamemap::location,paths> possible_moves;
possible_moves.insert(std::pair<gamemap::location,paths>(leader->first,leader_paths));
if(current_team().gold() < 20 && is_accessible(leader->first,enemy_dstsrc) == false) {
// See if we want to ward any enemy units off from getting our villages.
for(move_map::const_iterator i = enemy_dstsrc.begin(); i != enemy_dstsrc.end(); ++i) {
// If this is a village of ours, that an enemy can capture
// on their turn, and which we might be able to reach in two turns.
if(map_.is_village(i->first) && current_team().owns_village(i->first) &&
int(distance_between(i->first,leader->first)) <= leader->second.total_movement()*2) {
int current_distance = distance_between(i->first,leader->first);
location current_loc;
for(paths::routes_map::const_iterator j = leader_paths.routes.begin();
j != leader_paths.routes.end(); ++j) {
const int distance = distance_between(i->first,j->first);
if(distance < current_distance && is_accessible(j->first,enemy_dstsrc) == false) {
current_distance = distance;
current_loc = j->first;
}
}
// If this location is in range of the village,
// then we consider moving to it
if(current_loc.valid()) {
LOG_AI << "considering movement to " << str_cast(current_loc.x + 1)
<< "," << str_cast(current_loc.y+1);
unit_map temp_units(current_loc,leader->second);
const paths p(map_,state_,gameinfo_,temp_units,current_loc,teams_,false,false,current_team());
if(p.routes.count(i->first)) {
move_unit(leader->first,current_loc,possible_moves);
return;
}
}
}
}
}
// See if any friendly leaders can make it to our keep.
// If they can, then move off it, so that they can recruit if they want.
if(nearest_keep(leader->first) == leader->first) {
const location keep = leader->first;
std::pair<gamemap::location,unit> *temp_leader;
temp_leader = units_.extract(keep);
bool friend_can_reach_keep = false;
std::map<location,paths> friends_possible_moves;
move_map friends_srcdst, friends_dstsrc;
calculate_possible_moves(friends_possible_moves,friends_srcdst,friends_dstsrc,false,true);
for(move_map::const_iterator i = friends_dstsrc.begin(); i != friends_dstsrc.end(); ++i) {
if(i->first == keep) {
const unit_map::const_iterator itor = units_.find(i->second);
if(itor != units_.end() && itor->second.can_recruit()) {
friend_can_reach_keep = true;
break;
}
}
}
units_.add(temp_leader);
if(friend_can_reach_keep) {
// Find a location for our leader to vacate the keep to
location adj[6];
get_adjacent_tiles(keep,adj);
for(size_t n = 0; n != 6; ++n) {
// Vacate to the first location found that is on the board,
// our leader can move to, and no enemies can reach.
if(map_.on_board(adj[n]) &&
leader_paths.routes.count(adj[n]) != 0 &&
is_accessible(adj[n],enemy_dstsrc) == false) {
move_unit(keep,adj[n],possible_moves);
return;
}
}
}
}
// We didn't move: are we in trouble?
leader = find_leader(units_,team_num_);
if (!leader->second.has_moved() && leader->second.attacks_left()) {
std::map<gamemap::location,paths> dummy_possible_moves;
move_map fullmove_srcdst;
move_map fullmove_dstsrc;
calculate_possible_moves(dummy_possible_moves,fullmove_srcdst,fullmove_dstsrc,false,true,&avoided_locations());
if (should_retreat(leader->first, leader, fullmove_srcdst, fullmove_dstsrc, enemy_dstsrc, 0.5)) {
desperate_attack(leader->first);
}
}
}
bool ai::leader_can_reach_keep()
{
const unit_map::iterator leader = find_leader(units_,team_num_);
if(leader == units_.end() || leader->second.incapacitated()) {
return false;
}
const gamemap::location& start_pos = nearest_keep(leader->first);
if(start_pos.valid() == false) {
return false;
}
if(leader->first == start_pos) {
return true;
}
// Find where the leader can move
const paths leader_paths(map_,state_,gameinfo_,units_,leader->first,teams_,false,false,current_team());
return leader_paths.routes.count(start_pos) > 0;
}
int ai::rate_terrain(const unit& u, const gamemap::location& loc)
{
const t_translation::t_letter terrain = map_.get_terrain(loc);
const int defense = u.defense_modifier(terrain);
int rating = 100 - defense;
const int healing_value = 10;
const int friendly_village_value = 5;
const int neutral_village_value = 10;
const int enemy_village_value = 15;
if(map_.gives_healing(terrain) && u.get_ability_bool("regenerates",loc) == false) {
rating += healing_value;
}
if(map_.is_village(terrain)) {
const int owner = village_owner(loc,teams_);
if(owner + 1 == static_cast<int>(team_num_)) {
rating += friendly_village_value;
} else if(owner == -1) {
rating += neutral_village_value;
} else {
rating += enemy_village_value;
}
}
return rating;
}
const ai::defensive_position& ai::best_defensive_position(const gamemap::location& loc,
const move_map& dstsrc, const move_map& srcdst, const move_map& enemy_dstsrc)
{
const unit_map::const_iterator itor = units_.find(loc);
if(itor == units_.end()) {
static defensive_position pos;
pos.chance_to_hit = 0;
pos.vulnerability = pos.support = 0;
return pos;
}
const std::map<location,defensive_position>::const_iterator position =
defensive_position_cache_.find(loc);
if(position != defensive_position_cache_.end()) {
return position->second;
}
defensive_position pos;
pos.chance_to_hit = 100;
pos.vulnerability = 10000.0;
pos.support = 0.0;
typedef move_map::const_iterator Itor;
const std::pair<Itor,Itor> itors = srcdst.equal_range(loc);
for(Itor i = itors.first; i != itors.second; ++i) {
const int defense = itor->second.defense_modifier(map_.get_terrain(i->second));
if(defense > pos.chance_to_hit) {
continue;
}
const double vulnerability = power_projection(i->second,enemy_dstsrc);
const double support = power_projection(i->second,dstsrc);
if(defense < pos.chance_to_hit || support - vulnerability > pos.support - pos.vulnerability) {
pos.loc = i->second;
pos.chance_to_hit = defense;
pos.vulnerability = vulnerability;
pos.support = support;
}
}
defensive_position_cache_.insert(std::pair<location,defensive_position>(loc,pos));
return defensive_position_cache_[loc];
}
bool ai::is_accessible(const location& loc, const move_map& dstsrc) const
{
gamemap::location adj[6];
get_adjacent_tiles(loc,adj);
for(size_t n = 0; n != 6; ++n) {
if(dstsrc.count(adj[n]) > 0) {
return true;
}
}
return dstsrc.count(loc) > 0;
}
const std::set<gamemap::location>& ai::keeps()
{
if(keeps_.empty()) {
// Generate the list of keeps:
// iterate over the entire map and find all keeps.
for(size_t x = 0; x != size_t(map_.w()); ++x) {
for(size_t y = 0; y != size_t(map_.h()); ++y) {
const gamemap::location loc(x,y);
if(map_.is_keep(loc)) {
gamemap::location adj[6];
get_adjacent_tiles(loc,adj);
for(size_t n = 0; n != 6; ++n) {
if(map_.is_castle(adj[n])) {
keeps_.insert(loc);
break;
}
}
}
}
}
}
return keeps_;
}
const gamemap::location& ai::nearest_keep(const gamemap::location& loc)
{
const std::set<gamemap::location>& keeps = this->keeps();
if(keeps.empty()) {
static const gamemap::location dummy;
return dummy;
}
const gamemap::location* res = NULL;
int closest = -1;
for(std::set<gamemap::location>::const_iterator i = keeps.begin(); i != keeps.end(); ++i) {
const int distance = distance_between(*i,loc);
if(res == NULL || distance < closest) {
closest = distance;
res = &*i;
}
}
return *res;
}
const std::set<gamemap::location>& ai::avoided_locations()
{
if(avoid_.empty()) {
const config::child_list& avoids = current_team().ai_parameters().get_children("avoid");
for(config::child_list::const_iterator a = avoids.begin(); a != avoids.end(); ++a) {
const std::vector<location>& locs = parse_location_range((**a)["x"],(**a)["y"]);
for(std::vector<location>::const_iterator i = locs.begin(); i != locs.end(); ++i) {
avoid_.insert(*i);
}
}
if(avoid_.empty()) {
avoid_.insert(location());
}
}
return avoid_;
}
int ai::attack_depth()
{
if(attack_depth_ > 0) {
return attack_depth_;
}
const config& parms = current_team().ai_parameters();
attack_depth_ = maximum<int>(1,lexical_cast_default<int>(parms["attack_depth"],5));
return attack_depth_;
}