982 lines
29 KiB
C++
982 lines
29 KiB
C++
/* $Id$ */
|
|
/*
|
|
Copyright (C) 2003 - 2010 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 as published by
|
|
the Free Software Foundation; either version 2 of the License, 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
|
|
* Artificial intelligence - The computer commands the enemy.
|
|
*/
|
|
|
|
#include "ai.hpp"
|
|
|
|
#include "../actions.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 "../../resources.hpp"
|
|
#include "../../statistics.hpp"
|
|
#include "../../terrain_filter.hpp"
|
|
#include "../../unit_display.hpp"
|
|
#include "../../wml_exception.hpp"
|
|
|
|
#include "../../pathfind/pathfind.hpp"
|
|
|
|
#include <iterator>
|
|
#include <algorithm>
|
|
#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() const
|
|
{
|
|
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
|
|
{
|
|
config cfg;
|
|
cfg["ai_algorithm"]= "idle_ai";
|
|
return cfg;
|
|
}
|
|
|
|
|
|
int idle_ai::get_recursion_count() const
|
|
{
|
|
return recursion_counter_.get_count();
|
|
}
|
|
|
|
|
|
void idle_ai::play_turn()
|
|
{
|
|
game_events::fire("ai turn");
|
|
}
|
|
|
|
|
|
#ifdef _MSC_VER
|
|
#pragma warning(pop)
|
|
#endif
|
|
|
|
|
|
ai_default_recruitment_stage::recruit_situation_change_observer::recruit_situation_change_observer()
|
|
: valid_(false)
|
|
{
|
|
manager::add_recruit_list_changed_observer(this);
|
|
manager::add_turn_started_observer(this);
|
|
}
|
|
|
|
|
|
void ai_default_recruitment_stage::recruit_situation_change_observer::handle_generic_event(const std::string &/*event_name*/)
|
|
{
|
|
valid_ = false;
|
|
}
|
|
|
|
|
|
ai_default_recruitment_stage::recruit_situation_change_observer::~recruit_situation_change_observer()
|
|
{
|
|
manager::remove_recruit_list_changed_observer(this);
|
|
manager::remove_turn_started_observer(this);
|
|
}
|
|
|
|
|
|
bool ai_default_recruitment_stage::recruit_situation_change_observer::get_valid()
|
|
{
|
|
return valid_;
|
|
}
|
|
|
|
|
|
void ai_default_recruitment_stage::recruit_situation_change_observer::set_valid(bool valid)
|
|
{
|
|
valid_ = valid;
|
|
}
|
|
|
|
|
|
void ai_default_recruitment_stage::on_create() {
|
|
stage::on_create();
|
|
foreach (const config &c, cfg_.child_range("limit")) {
|
|
if (c.has_attribute("type") && c.has_attribute("max") ) {
|
|
maximum_counts_.insert(std::make_pair(c["type"],lexical_cast_default<int>(c["max"],0)));
|
|
}
|
|
}
|
|
}
|
|
|
|
config ai_default_recruitment_stage::to_config() const
|
|
{
|
|
config cfg = stage::to_config();
|
|
for (std::map<std::string,int>::const_iterator i = maximum_counts_.begin(); i!= maximum_counts_.end(); ++i) {
|
|
config lim;
|
|
lim["type"] = i->first;
|
|
lim["max"] = str_cast(i->second);
|
|
cfg.add_child("limit",lim);
|
|
}
|
|
return cfg;
|
|
}
|
|
|
|
|
|
void ai_default_recruitment_stage::analyze_all()
|
|
{
|
|
if (!recruit_situation_change_observer_.get_valid()) {
|
|
not_recommended_units_.clear();
|
|
unit_movement_scores_.clear();
|
|
unit_combat_scores_.clear();
|
|
analyze_potential_recruit_movements();
|
|
analyze_potential_recruit_combat();
|
|
recruit_situation_change_observer_.set_valid(true);
|
|
}
|
|
}
|
|
|
|
bool ai_default_recruitment_stage::recruit_usage(const std::string& usage)
|
|
{
|
|
raise_user_interact();
|
|
analyze_all();
|
|
|
|
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_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.
|
|
foreach (const std::string &name, current_team().recruits())
|
|
{
|
|
const unit_type *ut = unit_types.find(name);
|
|
if (!ut) continue;
|
|
// If usage is empty consider any unit.
|
|
if (usage.empty() || ut->usage() == usage) {
|
|
LOG_AI << name << " considered for " << usage << " recruitment\n";
|
|
found = true;
|
|
|
|
if (current_team().gold() - ut->cost() < min_gold) {
|
|
LOG_AI << name << " rejected, cost too high (cost: " << ut->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;
|
|
}
|
|
|
|
|
|
std::map<std::string,int>::iterator imc = maximum_counts_.find(name);
|
|
|
|
if (imc != maximum_counts_.end()) {
|
|
int count_active = 0;
|
|
for (unit_map::const_iterator u = resources::units->begin(); u != resources::units->end(); ++u) {
|
|
if (u->side() == get_side() && !u->incapacitated() && u->type_id() == name) {
|
|
++count_active;
|
|
}
|
|
}
|
|
|
|
if (count_active >= imc->second) {
|
|
LOG_AI << name << " rejected, too many in the field\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 " +
|
|
resources::state_of_game->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;
|
|
}
|
|
|
|
|
|
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;
|
|
};
|
|
|
|
}
|
|
|
|
class remove_wrong_targets {
|
|
public:
|
|
remove_wrong_targets(const readonly_context &context)
|
|
:avoid_(context.get_avoid()), map_(*resources::game_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_;
|
|
|
|
};
|
|
|
|
|
|
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_ = *resources::game_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::get_combat_score_vs(const unit_type& ut, const std::string &enemy_type_id, int &score, int &weighting, int hitpoints, int max_hitpoints) const
|
|
{
|
|
const unit_type *enemy_info = unit_types.find(enemy_type_id);
|
|
VALIDATE(enemy_info, "Unknown unit type : " + enemy_type_id + " while scoring units.");
|
|
int weight = ut.cost();
|
|
if ((hitpoints>0) && (max_hitpoints>0)) {
|
|
weight = weight * hitpoints / max_hitpoints;
|
|
}
|
|
|
|
weighting += weight;
|
|
score += compare_unit_types(ut, *enemy_info) * weight;
|
|
}
|
|
|
|
int ai_default_recruitment_stage::get_combat_score(const unit_type& ut) const
|
|
{
|
|
int score = 0, weighting = 0;
|
|
const unit_map & units_ = *resources::units;
|
|
for(unit_map::const_iterator j = units_.begin(); j != units_.end(); ++j) {
|
|
if (!current_team().is_enemy(j->side())) {
|
|
continue;
|
|
}
|
|
|
|
if (j->can_recruit()) {
|
|
|
|
team &enemy_team = (*resources::teams)[j->side() - 1];
|
|
const std::set<std::string> &recruits = enemy_team.recruits();
|
|
foreach (const std::string &rec, recruits) {
|
|
get_combat_score_vs(ut,rec,score,weighting,0,0);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
get_combat_score_vs(ut, j->type_id(), score, weighting, j->hitpoints(), j->max_hitpoints());
|
|
}
|
|
|
|
if(weighting != 0) {
|
|
score /= weighting;
|
|
}
|
|
return score;
|
|
}
|
|
|
|
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.
|
|
best_usage_.clear();
|
|
|
|
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 *info = unit_types.find(*i);
|
|
if (!info || not_recommended_units_.count(*i)) {
|
|
continue;
|
|
}
|
|
|
|
int score = get_combat_score(*info);
|
|
LOG_AI << "combat score of '" << *i << "': " << score << "\n";
|
|
unit_combat_scores_[*i] = score;
|
|
|
|
if(best_usage_.count(info->usage()) == 0 ||
|
|
score > best_usage_[info->usage()]) {
|
|
best_usage_[info->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 *info = unit_types.find(*i);
|
|
if (!info || not_recommended_units_.count(*i)) {
|
|
continue;
|
|
}
|
|
|
|
if(unit_combat_scores_[*i] + 600 < best_usage_[info->usage()]) {
|
|
LOG_AI << "recommending not to use '" << *i << "' because of poor combat performance "
|
|
<< unit_combat_scores_[*i] << "/" << best_usage_[info->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),
|
|
best_usage_(),
|
|
cfg_(cfg),
|
|
maximum_counts_(),
|
|
not_recommended_units_(),
|
|
recall_list_scores_(),
|
|
recruit_situation_change_observer_(),
|
|
unit_combat_scores_(),
|
|
unit_movement_scores_()
|
|
|
|
{
|
|
}
|
|
|
|
|
|
ai_default_recruitment_stage::~ai_default_recruitment_stage()
|
|
{
|
|
}
|
|
|
|
void ai_default_recruitment_stage::analyze_potential_recruit_movements()
|
|
{
|
|
unit_map &units_ = *resources::units;
|
|
gamemap &map_ = *resources::game_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->get_location());
|
|
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(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 *info = unit_types.find(*i);
|
|
if (!info) {
|
|
continue;
|
|
}
|
|
|
|
const unit_type &ut = *info;
|
|
///@todo 1.9: we give max movement, but recruited will get 0? Seems inaccurate
|
|
//but keep it like that for now
|
|
// pathfinding ignoring other units and terrain defense
|
|
const pathfind::move_type_path_calculator calc(ut.movement_type(), ut.movement(), ut.movement(), current_team(),map_);
|
|
|
|
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";
|
|
pathfind::plain_route route = a_star_search(start, t->loc, 100.0, &calc,
|
|
resources::game_map->w(), resources::game_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(ut.usage());
|
|
if(current_best == best_scores.end() || score < current_best->second) {
|
|
best_scores[ut.usage()] = score;
|
|
}
|
|
}
|
|
}
|
|
|
|
for(std::map<std::string,int>::iterator j = unit_movement_scores_.begin();
|
|
j != unit_movement_scores_.end(); ++j) {
|
|
|
|
const unit_type *info = unit_types.find(j->first);
|
|
|
|
if (!info) {
|
|
continue;
|
|
}
|
|
|
|
const int best_score = best_scores[info->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();
|
|
}
|
|
}
|
|
|
|
|
|
std::string ai_default_recruitment_stage::find_suitable_recall_id()
|
|
{
|
|
if (recall_list_scores_.empty()) {
|
|
return "";
|
|
}
|
|
std::string best_id = recall_list_scores_.back().first;
|
|
recall_list_scores_.pop_back();
|
|
return best_id;
|
|
}
|
|
|
|
class unit_combat_score_getter {
|
|
public:
|
|
unit_combat_score_getter(const ai_default_recruitment_stage &s)
|
|
: stage_(s)
|
|
{
|
|
}
|
|
std::pair<std::string, double> operator()(const unit &u) {
|
|
std::pair<std::string,int> p;
|
|
p.first = u.id();
|
|
const unit_type* u_type = u.type();
|
|
assert(u_type!=NULL);
|
|
|
|
double xp_ratio = 0;
|
|
if (u.can_advance() && (u.max_experience()>0)) {
|
|
xp_ratio = u.experience()/u.max_experience();
|
|
}
|
|
|
|
p.second = (1-xp_ratio) * stage_.get_combat_score(*u_type);
|
|
double recall_cost = game_config::recall_cost != 0 ? game_config::recall_cost : 1;
|
|
|
|
p.second *= static_cast<double>(u_type->cost())/recall_cost;
|
|
if (u.can_advance() && (xp_ratio>0) ) {
|
|
double best_combat_score_of_advancement = 0;
|
|
bool best_combat_score_of_advancement_found = false;
|
|
int best_cost = recall_cost;
|
|
foreach (const std::string &i, u.advances_to()) {
|
|
const unit_type *ut = unit_types.find(i);
|
|
if (!ut) {
|
|
continue;
|
|
}
|
|
|
|
int combat_score_of_advancement = stage_.get_combat_score(*ut);
|
|
if (!best_combat_score_of_advancement_found || (best_combat_score_of_advancement<combat_score_of_advancement)) {
|
|
best_combat_score_of_advancement = combat_score_of_advancement;
|
|
best_combat_score_of_advancement_found = true;
|
|
best_cost = ut->cost();
|
|
}
|
|
|
|
}
|
|
p.second += xp_ratio*best_combat_score_of_advancement*best_cost/recall_cost;
|
|
}
|
|
|
|
return p;
|
|
|
|
}
|
|
private:
|
|
const ai_default_recruitment_stage &stage_;
|
|
};
|
|
|
|
|
|
template <class T, class V>
|
|
bool smaller_mapped_value(const std::pair<T,V>& a, const std::pair<T,V>& b)
|
|
{
|
|
return a.second < b.second;
|
|
}
|
|
|
|
class bad_recalls_remover {
|
|
public:
|
|
bad_recalls_remover(const std::map<std::string, int>& unit_combat_scores)
|
|
: allow_any_(false), best_combat_score_()
|
|
{
|
|
std::map<std::string, int>::const_iterator cs = std::min_element(unit_combat_scores.begin(),unit_combat_scores.end(),&smaller_mapped_value<std::string,int>);
|
|
|
|
if (cs == unit_combat_scores.end()) {
|
|
allow_any_ = true;
|
|
} else {
|
|
best_combat_score_ = cs->second;
|
|
}
|
|
}
|
|
bool operator()(const std::pair<std::string,double>& p) {
|
|
if (allow_any_) {
|
|
return false;
|
|
}
|
|
if (p.second>=best_combat_score_) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private:
|
|
bool allow_any_;
|
|
double best_combat_score_;
|
|
};
|
|
|
|
|
|
class combat_score_less {
|
|
public:
|
|
bool operator()(const std::pair<std::string,double> &s1, const std::pair<std::string,double> &s2)
|
|
{
|
|
return s1.second<s2.second;
|
|
}
|
|
};
|
|
|
|
static void debug_print_recall_list_scores(const std::vector< std::pair<std::string,double> > &recall_list_scores,const char *message)
|
|
{
|
|
if (!lg::debug.dont_log(log_ai)) {
|
|
std::stringstream s;
|
|
s << message << std::endl;
|
|
for (std::vector< std::pair<std::string,double> >::const_iterator p = recall_list_scores.begin(); p!=recall_list_scores.end();++p) {
|
|
s << p->first << " ["<<p->second<<"]"<<std::endl;
|
|
}
|
|
DBG_AI << s.str();
|
|
}
|
|
|
|
|
|
}
|
|
|
|
bool ai_default_recruitment_stage::analyze_recall_list()
|
|
{
|
|
if (current_team().gold() < current_team().recall_cost() ) {
|
|
return false;
|
|
}
|
|
|
|
const std::vector<unit> &recalls = current_team().recall_list();
|
|
|
|
if (recalls.empty()) {
|
|
return false;
|
|
}
|
|
|
|
std::transform(recalls.begin(), recalls.end(), std::back_inserter< std::vector <std::pair<std::string,double> > > (recall_list_scores_), unit_combat_score_getter(*this) );
|
|
|
|
debug_print_recall_list_scores(recall_list_scores_,"Recall list, after scoring:");
|
|
|
|
recall_list_scores_.erase( std::remove_if(recall_list_scores_.begin(), recall_list_scores_.end(), bad_recalls_remover(unit_combat_scores_)), recall_list_scores_.end() );
|
|
|
|
debug_print_recall_list_scores(recall_list_scores_,"Recall list, after erase:");
|
|
|
|
if (recall_list_scores_.empty()) {
|
|
return false;
|
|
}
|
|
|
|
sort(recall_list_scores_.begin(),recall_list_scores_.end(),combat_score_less());
|
|
|
|
debug_print_recall_list_scores(recall_list_scores_,"Recall list, after sort (worst to best):");
|
|
|
|
return !(recall_list_scores_.empty());
|
|
}
|
|
|
|
|
|
bool ai_default_recruitment_stage::do_play_stage()
|
|
{
|
|
const unit_map &units_ = *resources::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->get_location());
|
|
|
|
analyze_all();
|
|
|
|
//handle recalls
|
|
//if there any recalls left which have a better combat score/cost ratio, get them
|
|
bool gamestate_changed = false;
|
|
std::string id;
|
|
if (analyze_recall_list()) {
|
|
while ( !(id = find_suitable_recall_id()).empty() ) {
|
|
|
|
recall_result_ptr recall_res = check_recall_action(id);
|
|
if (recall_res->is_ok()) {
|
|
recall_res->execute();
|
|
if (!recall_res->is_ok()) {
|
|
ERR_AI << "recall failed "<< std::endl;
|
|
break;
|
|
}
|
|
}
|
|
gamestate_changed |= recall_res->is_gamestate_changed();
|
|
|
|
}
|
|
}
|
|
|
|
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 = resources::game_map->villages();
|
|
for(std::vector<map_location>::const_iterator v = villages.begin(); v != villages.end(); ++v) {
|
|
const int owner = village_owner(*v,*resources::teams);
|
|
if(owner == -1) {
|
|
const size_t distance = distance_between(start_pos,*v);
|
|
|
|
bool closest = true;
|
|
for(std::vector<team>::const_iterator i = resources::teams->begin(); i != resources::teams->end(); ++i) {
|
|
const int index = i - resources::teams->begin() + 1;
|
|
const map_location& loc = resources::game_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->side() == get_side()) {
|
|
++unit_types[u->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.
|
|
|
|
while (recruit_usage(options[rand()%options.size()])) {
|
|
gamestate_changed = true;
|
|
options = get_recruitment_pattern();
|
|
if (options.empty()) {
|
|
options.push_back("");
|
|
}
|
|
}
|
|
|
|
return gamestate_changed;
|
|
}
|
|
|
|
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
|
|
|