wesnoth/src/ai/default/ai.cpp

1872 lines
56 KiB
C++

/* $Id$ */
/*
Copyright (C) 2003 - 2009 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 2
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/default/ai.cpp
* Artificial intelligence - The computer commands the enemy.
*/
#include "ai.hpp"
#include "../dfool/ai.hpp"
#include "../manager.hpp"
#include "../formula/ai.hpp"
#include "../../array.hpp"
#include "../../dialogs.hpp"
#include "../../foreach.hpp"
#include "../../game_end_exceptions.hpp"
#include "../../game_events.hpp"
#include "../../game_preferences.hpp"
#include "../../log.hpp"
#include "../../mouse_handler_base.hpp"
#include "../../replay.hpp"
#include "../../statistics.hpp"
#include "../../terrain_filter.hpp"
#include "../../unit_display.hpp"
#include "../../wml_exception.hpp"
#include <fstream>
static lg::log_domain log_ai("ai/general");
#define DBG_AI LOG_STREAM(debug, log_ai)
#define LOG_AI LOG_STREAM(info, log_ai)
#define WRN_AI LOG_STREAM(warn, log_ai)
#define ERR_AI LOG_STREAM(err, log_ai)
#ifdef _MSC_VER
#pragma warning(push)
//silence "inherits via dominance" warnings
#pragma warning(disable:4250)
#endif
namespace ai {
typedef util::array<map_location,6> adjacent_tiles_array;
idle_ai::idle_ai(readwrite_context &context, const config &cfg)
: cfg_(cfg), recursion_counter_(context.get_recursion_count())
{
init_readwrite_context_proxy(context);
}
std::string idle_ai::describe_self()
{
return "[idle_ai]";
}
void idle_ai::new_turn()
{
}
void idle_ai::switch_side(side_number side)
{
set_side(side);
}
config idle_ai::to_config() const
{
return config();
}
int idle_ai::get_recursion_count() const
{
return recursion_counter_.get_count();
}
void idle_ai::play_turn()
{
game_events::fire("ai turn");
}
/** Sample ai, with simple strategy. */
class sample_ai : public readwrite_context_proxy, public interface {
public:
sample_ai(readwrite_context &context, const config &cfg)
: cfg_(cfg), recursion_counter_(context.get_recursion_count()) {
init_readwrite_context_proxy(context);
}
virtual void play_turn() {
game_events::fire("ai turn");
do_attacks();
get_villages();
do_moves();
do_recruitment();
}
virtual std::string describe_self(){
return "[sample_ai]";
}
virtual int get_recursion_count() const
{
return recursion_counter_.get_count();
}
protected:
void do_attacks() {
std::map<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())) {
map_location adjacent_tiles[6];
get_adjacent_tiles(i->first,adjacent_tiles);
int best_defense = -1;
std::pair<map_location,map_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 map_location& dst = range.first->first;
const map_location& src = range.first->second;
const unit_map::const_iterator un = get_info().units.find(src);
const t_translation::t_terrain 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) {
bool gamestate_changed = false;
bool do_attack = false;
if (best_movement.first!=best_movement.second) {
move_result_ptr move_res = execute_move_action(best_movement.second,best_movement.first,true);
gamestate_changed |= move_res->is_gamestate_changed();
if (move_res->is_ok()) {
do_attack = true;
} else {
LOG_AI << "move_failed" << std::endl;
}
} else {
do_attack = true;
}
if (do_attack) {
attack_result_ptr attack_res = execute_attack_action(best_movement.first,i->first,-1);
gamestate_changed |= attack_res->is_gamestate_changed();
if (attack_res->is_ok()){
LOG_AI << "attack failed" << std::endl;
}
}
if (gamestate_changed) {
do_attacks();
}
return;
}
}
}
}
void get_villages() {
std::map<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_result_ptr move_res = execute_move_action(i->second,i->first,true);
if (move_res->is_ok()) {
LOG_AI << "move failed!" << std::endl;
}
if (move_res->is_gamestate_changed()) {
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<map_location,paths> possible_moves;
move_map srcdst, dstsrc;
calculate_possible_moves(possible_moves,srcdst,dstsrc,false);
int closest_distance = -1;
std::pair<map_location,map_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_result_ptr move_ptr = execute_move_action(closest_move.second,closest_move.first,true);
if (!move_ptr->is_ok()) {
LOG_AI << "move failed!" << std::endl;
}
if (move_ptr->is_gamestate_changed()) {
do_moves();
}
}
}
void switch_side(side_number side){
set_side(side);
}
config to_config() const
{
return config();
}
bool 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);
recruit_result_ptr recruit_res = execute_recruit_action(*i);
if (recruit_res->is_ok()) {
LOG_AI << "recruitment failed!" << std::endl;
}
if (recruit_res->is_gamestate_changed()) {
return do_recruitment();
}
return true;
}
return false;
}
private:
const config &cfg_;
recursion_counter recursion_counter_;
};
#ifdef _MSC_VER
#pragma warning(pop)
#endif
ai_default::ai_default(ai_context &context, const config &cfg) :
game_logic::formula_callable(),
cfg_(cfg),
recursion_counter_(context.get_recursion_count()),
threats_found_(false),
disp_(context.get_info().disp),
map_(context.get_info().map),
units_(context.get_info().units),
teams_(context.get_info().teams),
tod_manager_(context.get_info().tod_manager_),
consider_combat_(true),
recruiting_preferred_(0),
formula_ai_()
{
add_ref();
init_ai_context_proxy(context);
}
ai_default::~ai_default(){
}
void ai_default::switch_side(side_number side){
set_side(side);
}
void ai_default::new_turn()
{
invalidate_defensive_position_cache();
threats_found_ = false;
consider_combat_ = true;
clear_additional_targets();
invalidate_keeps_cache();
unit_stats_cache().clear();
}
std::string ai_default::describe_self(){
return "[default_ai]";
}
int ai_default::get_recursion_count() const
{
return recursion_counter_.get_count();
}
config ai_default::to_config() const
{
return config();
}
void ai_default_recruitment_stage::on_create() {
stage::on_create();
}
config ai_default_recruitment_stage::to_config() const
{
config cfg = stage::to_config();
return cfg;
}
bool ai_default_recruitment_stage::recruit_usage(const std::string& usage)
{
raise_user_interact();
const int min_gold = 0;
log_scope2(log_ai, "recruiting troops");
LOG_AI << "recruiting '" << usage << "'\n";
//make sure id, usage and cost are known for the coming evaluation of unit types
unit_type_data::types().build_all(unit_type::HELP_INDEX);
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 =
unit_type_data::types().begin(); i != unit_type_data::types().end(); ++i)
{
const std::string& name = i->second.id();
// If usage is empty consider any unit.
// DBG_AI << name << " considered\n";
if (i->second.usage() == usage || usage == "") {
if (!recruits.count(name)) {
// DBG_AI << name << " rejected, not in recruitment list\n";
continue;
}
LOG_AI << name << " considered for " << usage << " recruitment\n";
found = true;
if (current_team().gold() - i->second.cost() < min_gold) {
LOG_AI << name << " rejected, cost too high (cost: " << i->second.cost() << ", current gold: " << current_team().gold() <<", min_gold: " << min_gold << ")\n";
continue;
}
if (not_recommended_units_.count(name))
{
LOG_AI << name << " rejected, bad terrain or combat\n";
continue;
}
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();
recruit_result_ptr recruit_res = check_recruit_action(options[option]);
if (recruit_res->is_ok()) {
recruit_res->execute();
if (!recruit_res->is_ok()) {
ERR_AI << "recruitment failed "<< std::endl;
}
}
return recruit_res->is_gamestate_changed();
}
if (found) {
LOG_AI << "No available units to recruit that come under the price.\n";
} else if (usage != "") {
//FIXME: This message should be suppressed when WML author
//chooses the default recruitment pattern.
const std::string warning = "At difficulty level " +
get_info().game_state_.classification().difficulty + ", trying to recruit a:" +
usage + " but no unit of that type (usage=) is"
" available. Check the recruit and [ai]"
" recruitment_pattern keys for team '" +
current_team().name() + "' (" +
lexical_cast<std::string>(get_side()) + ")"
" against the usage key of the"
" units in question! Removing invalid"
" recruitment_pattern entry and continuing...\n";
WRN_AI << warning;
// Uncommented until the recruitment limiting macro can be fixed to not trigger this warning.
//lg::wml_error << warning;
//@fixme
//return current_team_w().remove_recruitment_pattern_entry(usage);
return false;
}
return false;
}
bool ai_default::multistep_move_possible(const map_location& from,
const map_location& to, const map_location& via,
const std::map<map_location,paths>& possible_moves) const
{
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<map_location,paths>::const_iterator moves = possible_moves.find(from);
if(moves != possible_moves.end()) {
LOG_AI << "found leader moves..\n";
// See if the unit can make it to 'via', and if it can,
// how much movement it will have left when it gets there.
paths::dest_vect::const_iterator itor =
moves->second.destinations.find(via);
if (itor != moves->second.destinations.end())
{
LOG_AI << "Can make it to keep with " << itor->move_left << " movement left.\n";
unit temp_unit(i->second);
temp_unit.set_movement(itor->move_left);
const temporary_unit_placer unit_placer(units_,via,temp_unit);
const paths unit_paths(map_,units_,via,teams_,false,false,current_team());
LOG_AI << "Found " << unit_paths.destinations.size() << " moves for temp leader.\n";
// See if this leader could make it back to the keep.
if (unit_paths.destinations.contains(to)) {
LOG_AI << "can make it back to the keep\n";
return true;
}
}
}
}
}
return false;
}
map_location ai_default::move_unit(map_location from, map_location to, bool &gamestate_changed)
{
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 map_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,get_possible_moves())) {
move_result_ptr move_to_keep_res = execute_move_action(from,start_pos,false);
gamestate_changed |= move_to_keep_res->is_gamestate_changed();
from = move_to_keep_res->get_unit_location();
if (!move_to_keep_res->is_ok()) {
LOG_AI << "first part of multistep move (getting to keep) failed" << std::endl;
return from;
}
}
if (map_.is_keep(from)) {
gamestate_changed |= do_recruitment();
}
}
move_result_ptr move_to_dst_ptr = check_move_action(from,to,true);
if (move_to_dst_ptr->is_ok()) {
move_to_dst_ptr->execute();
gamestate_changed |= move_to_dst_ptr->is_gamestate_changed();
if (!move_to_dst_ptr->is_ok()) {
LOG_AI << "full move failed" << std::endl;
}
//ambush ?
if (move_to_dst_ptr->get_move_spectator().get_ambusher().valid()) {
attack_result_ptr attack_res = check_attack_action(move_to_dst_ptr->get_unit_location(),move_to_dst_ptr->get_move_spectator().get_ambusher()->first,-1);
if (attack_res->is_ok()) {
attack_res->execute();
gamestate_changed |= attack_res->is_gamestate_changed();
if (!attack_res->is_ok()) {
LOG_AI << "attack on the ambusher (after move) failed" << std::endl;
}
}
}
return move_to_dst_ptr->get_unit_location();
}
return from;
}
namespace {
/** A structure for storing an item we're trying to protect. */
struct protected_item {
protected_item(double value, int radius, const map_location& loc) :
value(value), radius(radius), loc(loc) {}
double value;
int radius;
map_location loc;
};
}
void ai_default::find_threats()
{
if(threats_found_) {
return;
}
threats_found_ = true;
const config& parms = cfg_;
std::vector<protected_item> items;
// We want to protect our leader.
// FIXME: suokko tweaked these from (1.0, 20)->(2.0,15). Should this have been kept?
const unit_map::const_iterator leader = units_.find_leader(get_side());
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.
foreach (const config &p, parms.child_range("protect_location"))
{
items.push_back(protected_item(
lexical_cast_default<double>(p["value"], 1.0),
lexical_cast_default<int>(p["radius"], 20),
map_location(p, &get_info().game_state_)));
}
// Look for directions to protect a unit.
foreach (const config &p, parms.child_range("protect_unit"))
{
for(unit_map::const_iterator u = units_.begin(); u != units_.end(); ++u) {
if (game_events::unit_matches_filter(u, vconfig(p))) {
items.push_back(protected_item(
lexical_cast_default<double>(p["value"], 1.0),
lexical_cast_default<int>(p["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
&& !u->second.invisible(u->first, units_, teams_)) {
LOG_AI << "found threat target... " << u->first << "\n";
add_target(target(u->first, item.value * double(item.radius-distance) /
double(item.radius),target::THREAT));
}
}
}
}
void ai_default::play_turn()
{
// Protect against a memory over commitment:
/**
* @todo 2.0 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;
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_default::evaluate_recruiting_value(const map_location &leader_loc)
{
if (recruiting_preferred_ == 2)
{
recruiting_preferred_ = 0;
return;
}
if (get_number_of_possible_recruits_to_force_recruit()< 0.01f)
{
return;
}
float free_slots = 0.0f;
const float gold = static_cast<float>(current_team().gold());
const float unit_price = static_cast<float>(current_team().average_recruit_price());
if (map_.is_keep(leader_loc))
{
std::set<map_location> checked_hexes;
checked_hexes.insert(leader_loc);
free_slots = static_cast<float>(count_free_hexes_in_castle(leader_loc, checked_hexes));
} else {
map_location loc = nearest_keep(leader_loc);
if (units_.find(loc) == units_.end() && gold/unit_price > 1.0f)
{
free_slots -= static_cast<float>(get_number_of_possible_recruits_to_force_recruit());
}
}
recruiting_preferred_ = (gold/unit_price) - free_slots > get_number_of_possible_recruits_to_force_recruit();
DBG_AI << "recruiting preferred: " << (recruiting_preferred_?"yes":"no") <<
" units to recruit: " << (gold/unit_price) <<
" unit_price: " << unit_price <<
" free slots: " << free_slots <<
" limit: " << get_number_of_possible_recruits_to_force_recruit() << "\n";
}
void ai_default::do_move()
{
log_scope2(log_ai, "doing ai move");
invalidate_defensive_position_cache();
raise_user_interact();
typedef std::map<location,paths> moves_map;
const bool passive_leader_shares_keep = get_passive_leader_shares_keep();
const bool passive_leader = get_passive_leader()||passive_leader_shares_keep;
unit_map::iterator leader = units_.find_leader(get_side());
if (leader != units_.end())
{
evaluate_recruiting_value(leader->first);
}
// Execute goto-movements - first collect gotos in a list
std::vector<map_location> gotos;
for(unit_map::iterator ui = units_.begin(); ui != units_.end(); ++ui) {
if(ui->second.get_goto() == ui->first) {
ui->second.set_goto(map_location());
} else if(ui->second.side() == get_side() && map_.on_board(ui->second.get_goto())) {
gotos.push_back(ui->first);
}
}
for(std::vector<map_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 = get_dstsrc().begin(); i != get_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_result_ptr move_ptr = check_move_action(ui->first,closest_move.first);
if (move_ptr->is_ok()) {
move_ptr->execute();
if (move_ptr->is_ok()) {
WRN_AI << "'goto' move failed" << std::endl;
}
}
}
}
LOG_AI << "combat phase\n";
if(consider_combat_) {
LOG_AI << "combat...\n";
consider_combat_ = do_combat();
if(consider_combat_) {
do_move();
return;
}
}
move_leader_to_goals();
LOG_AI << "get villages phase\n";
// Iterator could be invalidated by combat analysis or move_leader_to_goals.
leader = units_.find_leader(get_side());
LOG_AI << "villages...\n";
if(get_villages(get_possible_moves(),get_dstsrc(),get_enemy_dstsrc(),leader)) {
do_move();
return;
}
LOG_AI << "healing...\n";
const bool healed_unit = get_healing();
if(healed_unit) {
do_move();
return;
}
LOG_AI << "retreat phase\n";
LOG_AI << "retreating...\n";
leader = units_.find_leader(get_side());
const bool retreated_unit = retreat_units(leader);
if(retreated_unit) {
do_move();
return;
}
find_threats();
LOG_AI << "move/targeting phase\n";
const bool met_invisible_unit = move_to_targets(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||passive_leader_shares_keep) {
map_location before = leader->first;
move_leader_to_keep();
leader = units_.find_leader(get_side());
if(leader == units_.end()) {
return;
}
// if leaders move is interrupted while we were trying to get to the keep, then do another move , but this next move should not be a recruiting, even if we originally wanted to recruit"
if (leader->first != before
&& leader->second.movement_left() > 0
&& recruiting_preferred_)
{
recruiting_preferred_ = 2;
do_move();
return;
}
}
if (map_.is_keep(leader->first))
{
if (do_recruitment())
{
do_move();
return;
} else if (recruiting_preferred_){
recruiting_preferred_ = 2;
do_move();
return;
}
}
if(!passive_leader||passive_leader_shares_keep) {
move_leader_after_recruit();
}
}
}
bool ai_default::do_combat()
{
const std::vector<attack_analysis> &analysis = get_attacks();
int 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>::const_iterator choice_it = analysis.end();
double choice_rating = -1000.0;
for(std::vector<attack_analysis>::const_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(get_aggression(),*this);
LOG_AI << "attack option rated at " << rating << " ("
<< get_aggression() << ")\n";
unit_map::unit_iterator u = units_.find(it->movements[0].first);
if (!u.valid()) {
continue;
}
if (u->second.attacks().size()==0) {
continue;
}
if (recruiting_preferred_) {
if (u->second.can_recruit()) {
LOG_AI << "Not fighting with leader because recruiting is more preferable\n";
continue;
}
}
if(rating > choice_rating) {
choice_it = it;
choice_rating = rating;
}
}
int time_taken = SDL_GetTicks() - ticks;
LOG_AI << "analysis took " << time_taken << " ticks\n";
// suokko tested the rating against current_team().caution()
// Bad mistake -- the AI became extremely reluctant to attack anything.
// Documenting this in case someone has this bright idea again...*don't*...
if(choice_rating > 0.0) {
map_location from = choice_it->movements[0].first;
map_location to = choice_it->movements[0].second;
map_location target_loc = choice_it->target;
// Never used:
// const unit_map::const_iterator tgt = units_.find(target_loc);
bool gamestate_changed = false;
const map_location arrived_at = move_unit(from,to,gamestate_changed);
if(arrived_at != to || units_.find(to) == units_.end()) {
WRN_AI << "unit moving to attack has ended up unexpectedly at "
<< arrived_at << " when moving to " << to << " from " << from << '\n';
return gamestate_changed;
}
attack_result_ptr attack_res = execute_attack_action(to, target_loc, -1);
gamestate_changed |= attack_res->is_gamestate_changed();
if (!attack_res->is_ok()) {
WRN_AI << "attack failed" << std::endl;
}
// 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)) {
LOG_AI << "found reinforcement target... " << target_loc << "\n";
//FIXME: sukko raised this value to 5.0. Is that correct?
add_target(target(target_loc,3.0,target::BATTLE_AID));
}
return gamestate_changed;
} else {
return false;
}
}
bool ai_default::get_healing()
{
// 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() == get_side() &&
(u.max_hitpoints() - u.hitpoints() >= game_config::poison_amount/2
|| u.get_state(unit::STATE_POISONED)) &&
!u.get_ability_bool("regenerate"))
{
// 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 = get_srcdst().equal_range(u_it->first);
double best_vulnerability = 100000.0;
// Make leader units more unlikely to move to vulnerable villages
const double leader_penalty = (u.can_recruit()?2.0:1.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, get_enemy_dstsrc());
LOG_AI << "found village with vulnerability: " << vuln << "\n";
if(vuln < best_vulnerability) {
best_vulnerability = vuln;
best_loc = it.first;
LOG_AI << "chose village " << dst << '\n';
}
}
++it.first;
}
// If we have found an eligible village,
// and we can move there without expecting to get whacked next turn:
if(best_loc != it.second && best_vulnerability*leader_penalty < u.hitpoints()) {
const location& src = best_loc->first;
const location& dst = best_loc->second;
LOG_AI << "moving unit to village for healing...\n";
bool gamestate_changed = false;
unit_map::iterator u = units_.find(move_unit(src,dst,gamestate_changed));
return gamestate_changed;
}
}
}
return false;
}
bool ai_default::should_retreat(const map_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_default::retreat_units(unit_map::const_iterator leader)
{
if (get_caution()<=0) {
return false;//note: this speeds up the evaluation - per-unit caution is not implemented anyway
}
// Get versions of the move map that assume that all units are at full movement
std::map<map_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, &get_avoid());
map_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() == get_side() &&
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,
get_enemy_dstsrc(), get_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 = get_srcdst().equal_range(i->first);
map_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 map_location& hex = itors.first->second;
const int defense = i->second.defense_modifier(map_.get_terrain(hex));
const double our_power = power_projection(hex,get_dstsrc());
const double their_power = power_projection(hex,get_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.type_id() << "' " << i->first
<< " -> " << best_pos << '\n';
bool gamestate_changed = false;
move_unit(i->first,best_pos,gamestate_changed);
#ifdef SUOKKO
// FIXME: This was in sukko's r29531 but backed out.
// Is it correct?
i->second.remove_movement_ai();
if (best_rating < 0.0)
add_target(target(best_pos, -3.0*best_rating, target::SUPPORT));
#endif
return gamestate_changed;
}
}
}
}
return false;
}
class remove_wrong_targets {
public:
remove_wrong_targets(const readonly_context &context)
:avoid_(context.get_avoid()), map_(context.get_info().map)
{
}
bool operator()(const target &t){
if (!map_.on_board(t.loc)) {
DBG_AI << "removing target "<< t.loc << " due to it not on_board" << std::endl;
return true;
}
if (t.value<=0) {
DBG_AI << "removing target "<< t.loc << " due to value<=0" << std::endl;
return true;
}
if (avoid_.match(t.loc)) {
DBG_AI << "removing target "<< t.loc << " due to 'avoid' match" << std::endl;
return true;
}
return false;
}
private:
const terrain_filter &avoid_;
const gamemap &map_;
};
bool ai_default::move_to_targets(unit_map::const_iterator leader)
{
LOG_AI << "finding targets...\n";
std::vector<target> targets;
for(;;) {
if(targets.empty()) {
targets = find_targets(leader,get_enemy_dstsrc());
targets.insert(targets.end(),additional_targets().begin(),
additional_targets().end());
LOG_AI << "Found " << targets.size() << " targets\n";
if(targets.empty()) {
return false;
}
}
targets.erase( std::remove_if(targets.begin(),targets.end(),remove_wrong_targets(*this)), targets.end() );
if(targets.empty()) {
return false;
}
LOG_AI << "choosing move with " << targets.size() << " targets\n";
std::pair<location,location> move = choose_move(targets, get_srcdst(),
get_dstsrc(), get_enemy_dstsrc());
LOG_AI << "choose_move ends with " << targets.size() << " targets\n";
for(std::vector<target>::const_iterator ittg = targets.begin();
ittg != targets.end(); ++ittg) {
assert(map_.on_board(ittg->loc));
}
if(move.first.valid() == false || move.second.valid() == false) {
break;
}
assert (map_.on_board(move.first)
&& map_.on_board(move.second));
LOG_AI << "move: " << move.first << " -> " << move.second << '\n';
bool gamestate_changed = false;
const map_location arrived_at = move_unit(move.first,move.second,gamestate_changed);
// 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) {
WRN_AI << "didn't arrive at destination\n";
return gamestate_changed;
}
}
return false;
}
int ai_default_recruitment_stage::average_resistance_against(const unit_type& a, const unit_type& b) const
{
int weighting_sum = 0, defense = 0;
gamemap &map_ = get_info().map;
const std::map<t_translation::t_terrain, size_t>& terrain =
map_.get_weighted_terrain_frequencies();
for (std::map<t_translation::t_terrain, 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) < unit_movement_type::UNREACHABLE) {
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_terrain, 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 = std::max<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 /= std::max<int>(1,std::min<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_default_recruitment_stage::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_default_recruitment_stage::analyze_potential_recruit_combat()
{
if(unit_combat_scores_.empty() == false ||
get_recruitment_ignore_bad_combat()) {
return;
}
log_scope2(log_ai, "analyze_potential_recruit_combat()");
// Records the best combat analysis for each usage type.
std::map<std::string,int> best_usage;
unit_map &units_ = get_info().units;
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 unit_type_data::unit_type_map::const_iterator info = unit_type_data::types().find_unit_type(*i);
if(info == unit_type_data::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 unit_type_data::unit_type_map::const_iterator enemy_info = unit_type_data::types().find_unit_type(un.type_id());
VALIDATE((enemy_info != unit_type_data::types().end()), "Unknown unit type : " + un.type_id() + " while scoring units.");
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 unit_type_data::unit_type_map::const_iterator info = unit_type_data::types().find_unit_type(*i);
if(info == unit_type_data::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 map_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:
map_location loc_;
};
}
ai_default_recruitment_stage::ai_default_recruitment_stage(ai_context &context, const config &cfg)
: stage(context,cfg),
cfg_(cfg),
unit_movement_scores_(),
not_recommended_units_(),
unit_combat_scores_()
{
}
ai_default_recruitment_stage::~ai_default_recruitment_stage()
{
}
void ai_default_recruitment_stage::analyze_potential_recruit_movements()
{
unit_map &units_ = get_info().units;
gamemap &map_ = get_info().map;
if(unit_movement_scores_.empty() == false ||
get_recruitment_ignore_bad_movement()) {
return;
}
const unit_map::const_iterator leader = units_.find_leader(get_side());
if(leader == units_.end()) {
return;
}
const map_location& start = nearest_keep(leader->first);
if(map_.on_board(start) == false) {
return;
}
log_scope2(log_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 unit_type_data::unit_type_map::const_iterator info = unit_type_data::types().find_unit_type(*i);
if(info == unit_type_data::types().end()) {
continue;
}
const unit temp_unit(&get_info().units, &info->second, get_side());
// since we now use the ignore_units switch, no need to use a empty unit_map
// unit_map units;
// const temporary_unit_placer placer(units,start,temp_unit);
// pathfinding ignoring other units and terrain defense
const shortest_path_calculator calc(temp_unit,current_team(),get_info().units,get_info().teams,map_,true,true);
int cost = 0;
int targets_reached = 0;
int targets_missed = 0;
for(std::vector<target>::const_iterator t = targets.begin(); t != targets.end(); ++t) {
LOG_AI << "analyzing '" << *i << "' getting to target...\n";
plain_route route = a_star_search(start, t->loc, 100.0, &calc,
get_info().map.w(), get_info().map.h());
if (!route.steps.empty()) {
LOG_AI << "made it: " << route.move_cost << "\n";
cost += route.move_cost;
++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 unit_type_data::unit_type_map::const_iterator info =
unit_type_data::types().find_unit_type(j->first);
if(info == unit_type_data::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();
}
}
bool ai_default::do_recruitment()
{
raise_user_interact();
stage_ptr r = get_recruitment(*this);
if (r) {
return r->play_stage();
}
ERR_AI << "no recruitment aspect - skipping recruitment" << std::endl;
return false;
}
bool ai_default_recruitment_stage::do_play_stage()
{
const unit_map &units_ = get_info().units;
const unit_map::const_iterator leader = units_.find_leader(get_side());
if(leader == units_.end()) {
return false;
}
const map_location& start_pos = nearest_keep(leader->first);
analyze_potential_recruit_movements();
analyze_potential_recruit_combat();
std::vector<std::string> options = get_recruitment_pattern();
if (std::count(options.begin(), options.end(), "scout") > 0) {
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<map_location>& villages = get_info().map.villages();
for(std::vector<map_location>::const_iterator v = villages.begin(); v != villages.end(); ++v) {
const int owner = village_owner(*v,get_info().teams);
if(owner == -1) {
const size_t distance = distance_between(start_pos,*v);
bool closest = true;
for(std::vector<team>::const_iterator i = get_info().teams.begin(); i != get_info().teams.end(); ++i) {
const int index = i - get_info().teams.begin() + 1;
const map_location& loc = get_info().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 = get_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() == get_side()) {
++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"];
}
}
// 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.
bool ret = false;
while (recruit_usage(options[rand()%options.size()])) {
ret = true;
options = get_recruitment_pattern();
if (options.empty()) {
options.push_back("");
}
}
return ret;
}
void ai_default::move_leader_to_goals()
{
const config &goal = get_leader_goal();
if (!goal) {
LOG_AI << "No goal found\n";
return;
}
if (goal.empty()) {
return;
}
const map_location dst(goal, &get_info().game_state_);
if (!dst.valid()) {
ERR_AI << "Invalid goal: "<<std::endl<<goal;
return;
}
const unit_map::iterator leader = units_.find_leader(get_side());
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_);
plain_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_, units_, leader->first,
teams_, false, false, current_team());
std::map<map_location,paths> possible_moves;
possible_moves.insert(std::pair<map_location,paths>(leader->first,leader_paths));
map_location loc;
foreach (const map_location &l, route.steps)
{
if (leader_paths.destinations.contains(l) &&
power_projection(l, get_enemy_dstsrc()) < double(leader->second.hitpoints() / 2))
{
loc = l;
}
}
if(loc.valid()) {
LOG_AI << "Moving leader to goal\n";
bool gamestate_changed = false;
move_unit(leader->first,loc,gamestate_changed);
if (!gamestate_changed) {
ERR_AI << "side: "<< get_side() << " trying to move leader to goal has failed" << std::endl;
}
}
}
void ai_default::move_leader_after_recruit()
{
unit_map::iterator leader = units_.find_leader(get_side());
if(leader == units_.end() || leader->second.incapacitated() || leader->second.movement_left() == 0) {
return;
}
const bool passive_leader_shares_keep = get_passive_leader_shares_keep();
const bool passive_leader = get_passive_leader()||passive_leader_shares_keep;
const paths leader_paths(map_, units_, leader->first,
teams_, false, false, current_team());
std::map<map_location,paths> possible_moves;
possible_moves.insert(std::pair<map_location,paths>(leader->first,leader_paths));
if(!passive_leader && current_team().gold() < 20 && is_accessible(leader->first,get_enemy_dstsrc()) == false) {
// See if we want to ward any enemy units off from getting our villages.
for(move_map::const_iterator i = get_enemy_dstsrc().begin(); i != get_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;
foreach (const paths::step &dest, leader_paths.destinations)
{
const int distance = distance_between(i->first, dest.curr);
if (distance < current_distance &&
!is_accessible(dest.curr, get_enemy_dstsrc()))
{
current_distance = distance;
current_loc = dest.curr;
}
}
// 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;
temp_units.add(current_loc, leader->second);
const paths p(map_, temp_units, current_loc, teams_, false,
false, current_team());
if (p.destinations.contains(i->first))
{
bool gamestate_changed = false;
move_unit(leader->first,current_loc,gamestate_changed);
if (!gamestate_changed) {
ERR_AI << "moving leader after recruit failed"<< std::endl;
}
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((!passive_leader || passive_leader_shares_keep) && nearest_keep(leader->first) == leader->first) {
const location keep = leader->first;
std::pair<map_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_.insert(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.destinations.contains(adj[n]) &&
!is_accessible(adj[n], get_enemy_dstsrc()))
{
bool gamestate_changed = false;
map_location new_loc = move_unit(keep,adj[n],gamestate_changed);
if (!gamestate_changed) {
ERR_AI << "moving leader after recruit failed" << std::endl;
}
if (new_loc!=keep) {
return;
}
}
}
}
}
// We didn't move: are we in trouble?
leader = units_.find_leader(get_side());
if (!passive_leader && !leader->second.has_moved() && leader->second.attacks_left()) {
std::map<map_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,&get_avoid());
if (should_retreat(leader->first, leader, fullmove_srcdst, fullmove_dstsrc, get_enemy_dstsrc(), 0.5)) {
desperate_attack(leader->first);
}
}
}
bool ai_default::is_accessible(const location& loc, const move_map& dstsrc) const
{
map_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;
}
variant ai_default::get_value(const std::string &/*key*/) const
{
return variant();
}
void ai_default::get_inputs(std::vector<game_logic::formula_input>* /*inputs*/) const
{
}
variant attack_analysis::get_value(const std::string& key) const
{
using namespace game_logic;
if(key == "target") {
return variant(new location_callable(target));
} else if(key == "movements") {
std::vector<variant> res;
for(size_t n = 0; n != movements.size(); ++n) {
map_formula_callable* item = new map_formula_callable(NULL);
item->add("src", variant(new location_callable(movements[n].first)));
item->add("dst", variant(new location_callable(movements[n].second)));
res.push_back(variant(item));
}
return variant(&res);
} else if(key == "units") {
std::vector<variant> res;
for(size_t n = 0; n != movements.size(); ++n) {
res.push_back(variant(new location_callable(movements[n].first)));
}
return variant(&res);
} else if(key == "target_value") {
return variant(static_cast<int>(target_value*1000));
} else if(key == "avg_losses") {
return variant(static_cast<int>(avg_losses*1000));
} else if(key == "chance_to_kill") {
return variant(static_cast<int>(chance_to_kill*100));
} else if(key == "avg_damage_inflicted") {
return variant(static_cast<int>(avg_damage_inflicted));
} else if(key == "target_starting_damage") {
return variant(target_starting_damage);
} else if(key == "avg_damage_taken") {
return variant(static_cast<int>(avg_damage_taken));
} else if(key == "resources_used") {
return variant(static_cast<int>(resources_used));
} else if(key == "terrain_quality") {
return variant(static_cast<int>(terrain_quality));
} else if(key == "alternative_terrain_quality") {
return variant(static_cast<int>(alternative_terrain_quality));
} else if(key == "vulnerability") {
return variant(static_cast<int>(vulnerability));
} else if(key == "support") {
return variant(static_cast<int>(support));
} else if(key == "leader_threat") {
return variant(leader_threat);
} else if(key == "uses_leader") {
return variant(uses_leader);
} else if(key == "is_surrounded") {
return variant(is_surrounded);
} else {
return variant();
}
}
void attack_analysis::get_inputs(std::vector<game_logic::formula_input>* inputs) const
{
using namespace game_logic;
inputs->push_back(formula_input("target", FORMULA_READ_ONLY));
inputs->push_back(formula_input("movements", FORMULA_READ_ONLY));
inputs->push_back(formula_input("units", FORMULA_READ_ONLY));
inputs->push_back(formula_input("target_value", FORMULA_READ_ONLY));
inputs->push_back(formula_input("avg_losses", FORMULA_READ_ONLY));
inputs->push_back(formula_input("chance_to_kill", FORMULA_READ_ONLY));
inputs->push_back(formula_input("avg_damage_inflicted", FORMULA_READ_ONLY));
inputs->push_back(formula_input("target_starting_damage", FORMULA_READ_ONLY));
inputs->push_back(formula_input("avg_damage_taken", FORMULA_READ_ONLY));
inputs->push_back(formula_input("resources_used", FORMULA_READ_ONLY));
inputs->push_back(formula_input("terrain_quality", FORMULA_READ_ONLY));
inputs->push_back(formula_input("alternative_terrain_quality", FORMULA_READ_ONLY));
inputs->push_back(formula_input("vulnerability", FORMULA_READ_ONLY));
inputs->push_back(formula_input("support", FORMULA_READ_ONLY));
inputs->push_back(formula_input("leader_threat", FORMULA_READ_ONLY));
inputs->push_back(formula_input("uses_leader", FORMULA_READ_ONLY));
inputs->push_back(formula_input("is_surrounded", FORMULA_READ_ONLY));
}
} //end of namespace ai