Get rid of last user of evaluate_battle_stats: the AI.

May effect performance.  ai::choose_weapon now simply uses
best_attack_weapon, which is a little smarter about poison and slow.

Some code movement because best_attack_weapon needs unit map to
indicate reality (for filters it needs exact location of units in
combat).

AI now uses attack_prediction code, not 50 iterations, to predict
results.  This will handle specials better, but there may be
incidental changes to AI's decision-making.
This commit is contained in:
Rusty Russell 2006-05-15 11:48:04 +00:00
parent 0f69a33a32
commit f22beebe51
5 changed files with 160 additions and 889 deletions

View file

@ -224,509 +224,6 @@ gamemap::location under_leadership(const units_map& units,
return abil.highest("value").second;
}
double pr_atleast(int m, double p, int n, int d)
{
// calculate Pr[A does damage in [m,...)], where
// p probability to hit, n swings, d damage/swing
double P = 0.0;
// 0 damage can happen when unit has no attack of this type
if(d == 0)
return (m <= 0) ? 1.0 : P;
for(int k = (m + d - 1)/d; k <= n; ++k) {
double r = 1.0;
const int k2 = (k > n - k) ? (n - k) : k;
for(int i = 0; i < k2; ++i) { r *= (n-i); r /= (k2-i); }
P += r * pow(p, k) * pow(1-p, n-k);
}
return P;
}
double pr_between(int mn, int mx, double p, int n, int d)
{
// calculate Pr[A does damage in [mn,mx)], where
// p probability to hit, n swings, d damage/swing
return pr_atleast(mn, p, n, d) - pr_atleast(mx, p, n, d);
}
int reduce(int i, int u, int v)
{
// i-th swingpair, but reduce since u < v
if(i == 0)
return i;
else
return ((i-1) / v)*u + minimum<int>(u, ((i-1) % v) + 1);
}
double pr_kills_during(const int hpa, const int dmga, const double pa,
const int swa, const int hpb, const int dmgb, const double pb,
const int swb, const int n, const bool second)
{
if ((swa < swb) && (swa < (n-1) % swb + 1)) // not our move
return 0.0;
// A kills B during swing n, and is it second?
// take into account where one unit doesn't get all n swings
const double t1 = pr_between(hpb - dmga, hpb, pa,
(swa<swb) ? reduce(n-1, swa, swb) : (n-1), dmga);
const int n2 = second ? n : (n - 1);
const double t2 = 1.0 - pr_atleast(hpa, pb,
(swa>swb) ? reduce(n2, swb, swa) : n2, dmgb);
return t1 * pa * t2;
}
battle_stats evaluate_battle_stats(const gamemap& map,
std::vector<team>& teams,
const gamemap::location& attacker,
const gamemap::location& defender,
int attack_with,
units_map& units,
const gamestatus& state,
const game_data& gamedata,
gamemap::TERRAIN attacker_terrain_override,
battle_stats_strings *strings)
{
battle_stats res;
LOG_NG << "Evaluating battle stats...\n";
res.attack_with = attack_with;
if (strings)
strings->defend_name = _("none");
const units_map::const_iterator a = units.find(attacker);
const units_map::const_iterator d = units.find(defender);
wassert(a != units.end());
wassert(d != units.end());
const gamemap::TERRAIN attacker_terrain = attacker_terrain_override ?
attacker_terrain_override : map[attacker.x][attacker.y];
const gamemap::TERRAIN defender_terrain = map[defender.x][defender.y];
res.attacker_hp = a->second.hitpoints();
res.defender_hp = d->second.hitpoints();
res.chance_to_hit_attacker = a->second.defense_modifier(attacker_terrain);
res.chance_to_hit_defender = d->second.defense_modifier(defender_terrain);
const std::vector<attack_type>& attacker_attacks = a->second.attacks();
const std::vector<attack_type>& defender_attacks = d->second.attacks();
wassert((unsigned)attack_with < attacker_attacks.size());
const attack_type& attack = attacker_attacks[attack_with];
double best_defend_rating = 0.0;
int defend_with = -1;
res.ndefends = 0;
LOG_NG << "Finding defender weapon...\n";
for(int defend_option = 0; defend_option != int(defender_attacks.size()); ++defend_option) {
if(defender_attacks[defend_option].range() == attack.range()) {
if (defender_attacks[defend_option].defense_weight() > 0) {
const attack_type& defend = defender_attacks[defend_option];
attack.set_specials_context(attacker,defender,&gamedata,&units,&map,&state,&teams,true,&defend);
defend.set_specials_context(attacker,defender,&gamedata,&units,&map,&state,&teams,false,&attack);
int d_nattacks = defend.num_attacks();
unit_ability_list swarm = defend.get_specials("attacks");
if(!swarm.empty()) {
int swarm_min_attacks = swarm.highest("attacks_min").first;
int swarm_max_attacks = swarm.highest("attacks_max",d_nattacks).first;
int hitp = d->second.hitpoints();
int mhitp = d->second.max_hitpoints();
d_nattacks = swarm_min_attacks + (swarm_max_attacks - swarm_min_attacks) * hitp / mhitp;
}
// calculate damage
int bonus = 100;
int divisor = 100;
int base_damage = defend.damage();
int resistance_modifier = a->second.damage_from(defend,true,a->first);
bool backstab = backstab_check(d->first,a->first,units,teams);
{ // modify damage
unit_ability_list dmg_specials = defend.get_specials("damage");
unit_abilities::effect dmg_effect(dmg_specials,base_damage,backstab);
base_damage = dmg_effect.get_composite_value();
}
const int tod_modifier = combat_modifier(state,units,d->first,d->second.alignment(),map);
bonus += tod_modifier;
int leader_bonus = 0;
if (under_leadership(units, defender, &leader_bonus).valid()) {
bonus += leader_bonus;
}
if (utils::string_bool(d->second.get_state("slowed"))) {
divisor *= 2;
}
bonus *= resistance_modifier;
divisor *= 100;
const int final_damage = round_damage(base_damage, bonus, divisor);
const double rating = a->second.damage_from(defender_attacks[defend_option],true,a->first)
*final_damage
*d_nattacks
*defender_attacks[defend_option].defense_weight();
if(defend_with == -1 || rating > best_defend_rating) {
best_defend_rating = rating;
defend_with = defend_option;
}
}
}
}
res.defend_with = defend_with;
config tmp_config;
static attack_type no_weapon(tmp_config,"fake_attack","");
const attack_type& defend = defend_with == -1 ? no_weapon : defender_attacks[defend_with];
attack.set_specials_context(attacker,defender,&gamedata,&units,&map,&state,&teams,true,&defend);
defend.set_specials_context(attacker,defender,&gamedata,&units,&map,&state,&teams,false,&attack);
LOG_NG << "getting weapon specials...\n";
static const std::string to_the_death_string("berserk");
res.rounds = attack.get_specials(to_the_death_string).highest("value").first;
res.defender_strikes_first = false;
unit_ability_list plague = attack.get_specials("plague");
static const std::string plague_string("plague");
res.attacker_plague = !utils::string_bool(d->second.get_state("not_living")) &&
(!plague.empty()) &&
strcmp(d->second.undead_variation().c_str(),"null") &&
!map.is_village(defender);
if(!plague.empty()) {
if((*plague.cfgs.front().first)["type"] == "") {
res.attacker_plague_type = a->second.id();
} else {
res.attacker_plague_type = (*plague.cfgs.front().first)["type"];
}
}
res.defender_plague = false;
static const std::string slow_string("slow");
res.attacker_slows = attack.get_special_bool(slow_string);
static const std::string poison_string("poison");
res.attacker_poisons = attack.get_special_bool(poison_string);
static const std::string stones_string("stones");
res.attacker_stones = attack.get_special_bool(stones_string);
{ // modify chance to hit
bool backstab = backstab_check(a->first,d->first,units,teams);
unit_ability_list cth_specials = attack.get_specials("chance_to_hit");
unit_abilities::effect cth_effects(cth_specials,res.chance_to_hit_defender,backstab);
res.chance_to_hit_defender = cth_effects.get_composite_value();
}
// compute swarm attacks;
unit_ability_list swarm = attack.get_specials("attacks");
if(!swarm.empty()) {
int swarm_min_attacks = swarm.highest("attacks_min").first;
int swarm_max_attacks = swarm.highest("attacks_max",attack.num_attacks()).first;
int hitp = a->second.hitpoints();
int mhitp = a->second.max_hitpoints();
res.nattacks = swarm_min_attacks + (swarm_max_attacks - swarm_min_attacks) * hitp / mhitp;
} else {
res.nattacks = attack.num_attacks();
}
if (strings) {
strings->attack_name = attack.name();
strings->attack_type = egettext(attack.type().c_str());
strings->attack_special = attack.weapon_specials();
strings->attack_icon = attack.icon();
strings->range = gettext(N_(attack.range().c_str()));
}
const bool counterattack = defend_with != -1;
static const std::string EMPTY_COLUMN = std::string(1, COLUMN_SEPARATOR) + ' ' + COLUMN_SEPARATOR;
res.damage_attacker_takes = 0;
res.amount_attacker_drains = 0;
res.amount_defender_drains = 0;
if (counterattack) {
res.rounds = maximum<int>(res.rounds,defend.get_specials(to_the_death_string).highest("value").first);
bool backstab = backstab_check(d->first,a->first,units,teams);
{ // modify chance to hit
unit_ability_list cth_specials = defend.get_specials("chance_to_hit");
unit_abilities::effect cth_effects(cth_specials,res.chance_to_hit_attacker,backstab);
res.chance_to_hit_attacker = cth_effects.get_composite_value();
}
int bonus = 100;
int divisor = 100;
int base_damage = defend.damage();
int resistance_modifier = a->second.damage_from(defend,true,a->first);
{ // modify damage
unit_ability_list dmg_specials = defend.get_specials("damage");
unit_abilities::effect dmg_effects(dmg_specials,base_damage,backstab);
base_damage = dmg_effects.get_composite_value();
}
if (strings) {
std::stringstream str_base;
str_base << _("base damage") << COLUMN_SEPARATOR << base_damage;
strings->defend_calculations.push_back(str_base.str());
}
const int tod_modifier = combat_modifier(state,units,d->first,d->second.alignment(),map);
bonus += tod_modifier;
if (strings && tod_modifier != 0) {
std::stringstream str_mod;
const time_of_day& tod = timeofday_at(state,units,d->first,map);
str_mod << tod.name << EMPTY_COLUMN << (tod_modifier > 0 ? "+" : "") << tod_modifier << '%';
strings->defend_calculations.push_back(str_mod.str());
}
int leader_bonus = 0;
if (under_leadership(units, defender, &leader_bonus).valid()) {
bonus += leader_bonus;
if (strings) {
std::stringstream str;
str << _("leadership") << EMPTY_COLUMN << '+' << leader_bonus << '%';
strings->defend_calculations.push_back(str.str());
}
}
/*
if (charge) {
bonus *= 2;
if (strings) {
std::stringstream str;
str << _("charge") << EMPTY_COLUMN << _("Doubled");
strings->defend_calculations.push_back(str.str());
}
}
*/
if (utils::string_bool(d->second.get_state("slowed"))) {
divisor *= 2;
if (strings) {
std::stringstream str;
str << _("slowed") << EMPTY_COLUMN << _("Halved");
strings->defend_calculations.push_back(str.str());
}
}
if (strings && resistance_modifier != 100) {
const int resist = resistance_modifier - 100;
std::stringstream str_resist;
str_resist << gettext(resist < 0 ? N_("attacker resistance vs") : N_("attacker vulnerability vs"))
<< ' ' << gettext(defend.type().c_str()) << EMPTY_COLUMN
<< (resist > 0 ? "+" : "") << resist << '%';
strings->defend_calculations.push_back(str_resist.str());
}
bonus *= resistance_modifier;
divisor *= 100;
const int final_damage = round_damage(base_damage, bonus, divisor);
res.damage_attacker_takes = final_damage;
if (strings) {
const int difference = final_damage - base_damage;
std::stringstream str;
str << _("total damage") << COLUMN_SEPARATOR << res.damage_attacker_takes
<< COLUMN_SEPARATOR << (difference >= 0 ? "+" : "")
<< difference;
strings->defend_calculations.push_back(str.str());
}
// compute swarm attacks;
unit_ability_list swarm = defend.get_specials("attacks");
if(!swarm.empty()) {
int swarm_min_attacks = swarm.highest("attacks_min").first;
int swarm_max_attacks = swarm.highest("attacks_max",defend.num_attacks()).first;
int hitp = d->second.hitpoints();
int mhitp = d->second.max_hitpoints();
res.ndefends = swarm_min_attacks + (swarm_max_attacks - swarm_min_attacks) * hitp / mhitp;
} else {
res.ndefends = defend.num_attacks();
}
if (strings) {
strings->defend_name = defend.name();
strings->defend_type = egettext(defend.type().c_str());
strings->defend_special = defend.weapon_specials();
strings->defend_icon = defend.icon();
}
//if the defender drains, and the attacker is a living creature, then
//the defender will drain for half the damage it does
if (defend.get_special_bool("drains") && !utils::string_bool(a->second.get_state("not_living"))) {
res.amount_defender_drains = res.damage_attacker_takes/2;
}
unit_ability_list defend_plague = attack.get_specials("plague");
res.defender_plague = !utils::string_bool(a->second.get_state("not_living")) &&
(!defend_plague.empty()) &&
strcmp(a->second.undead_variation().c_str(),"null") &&
!map.is_village(attacker);
if(!plague.empty()) {
if((*plague.cfgs.front().first)["type"] == "") {
res.defender_plague_type = d->second.id();
} else {
res.defender_plague_type = (*plague.cfgs.front().first)["type"];
}
}
res.defender_slows = (defend.get_special_bool(slow_string));
res.defender_poisons = (defend.get_special_bool(poison_string));
res.defender_stones = (defend.get_special_bool(stones_string));
static const std::string first_strike = "firststrike";
res.defender_strikes_first = defend.get_special_bool(first_strike) && !attack.get_special_bool(first_strike);
}
int bonus = 100;
int divisor = 100;
int base_damage = attack.damage();
int resistance_modifier = d->second.damage_from(attack,false,d->first);
{ // modify damage
bool backstab = backstab_check(a->first,d->first,units,teams);
{ // modify damage
unit_ability_list dmg_specials = attack.get_specials("damage");
unit_abilities::effect dmg_effect(dmg_specials,base_damage,backstab);
base_damage = dmg_effect.get_composite_value();
}
}
if (strings) {
std::stringstream str_base;
str_base << _("base damage") << COLUMN_SEPARATOR << base_damage << COLUMN_SEPARATOR;
strings->attack_calculations.push_back(str_base.str());
}
const int tod_modifier = combat_modifier(state,units,a->first,a->second.alignment(),map);
bonus += tod_modifier;
if (strings && tod_modifier != 0) {
std::stringstream str_mod;
const time_of_day& tod = timeofday_at(state,units,a->first,map);
str_mod << tod.name << EMPTY_COLUMN << (tod_modifier > 0 ? "+" : "") << tod_modifier << '%';
strings->attack_calculations.push_back(str_mod.str());
}
int leader_bonus = 0;
if (under_leadership(units,attacker,&leader_bonus).valid()) {
bonus += leader_bonus;
if (strings) {
std::stringstream str;
str << _("leadership") << EMPTY_COLUMN << '+' << leader_bonus << '%';
strings->attack_calculations.push_back(str.str());
}
}
if (strings && resistance_modifier != 100) {
const int resist = resistance_modifier - 100;
std::stringstream str_resist;
str_resist << gettext(resist < 0 ? N_("defender resistance vs") : N_("defender vulnerability vs"))
<< ' ' << gettext(attack.type().c_str());
str_resist << EMPTY_COLUMN
<< (resist > 0 ? "+" : "") << resist << '%';
strings->attack_calculations.push_back(str_resist.str());
}
if (utils::string_bool(a->second.get_state("slowed"))) {
divisor *= 2;
if (strings) {
std::stringstream str;
str << _("slowed") << EMPTY_COLUMN << _("Halved");
strings->attack_calculations.push_back(str.str());
}
}
bonus *= resistance_modifier;
divisor *= 100;
const int final_damage = round_damage(base_damage, bonus, divisor);
res.damage_defender_takes = final_damage;
if (strings) {
const int difference = final_damage - base_damage;
std::stringstream str;
str << _("total damage") << COLUMN_SEPARATOR << res.damage_defender_takes
<< COLUMN_SEPARATOR << (difference >= 0 ? "+" : "")
<< difference;
strings->attack_calculations.push_back(str.str());
}
//if the attacker drains, and the defender is a living creature, then
//the attacker will drain for half the damage it does
if(attack.get_special_bool("drains") && utils::string_bool(d->second.get_state("not_living"))) {
res.amount_attacker_drains = res.damage_defender_takes/2;
}
// FIXME: doesn't take into account berserk+slow or drain
if (strings && res.amount_attacker_drains == 0 &&
res.amount_defender_drains == 0 &&
!(res.rounds &&
(res.attacker_slows || res.defender_slows)))
{
const int maxrounds = (res.rounds ? res.rounds : 1);
const int hpa = res.attacker_hp;
const int hpb = res.defender_hp;
const int dmga = res.damage_defender_takes;
const int dmgb = res.damage_attacker_takes;
const double pa = res.chance_to_hit_defender/100.0;
const double pb = res.chance_to_hit_attacker/100.0;
const int swa = res.nattacks;
const int swb = res.ndefends;
double P1 = 0;
for(int n = 1; n <= maxrounds*maximum<int>(swa,swb); ++n)
P1 += pr_kills_during(hpa, dmga, pa, swa,
hpb, dmgb, pb, swb, n, res.defender_strikes_first);
const double P3 = (1.0 - pr_atleast(hpb,pa,maxrounds*swa,dmga))
* (1.0 - pr_atleast(hpa,pb,maxrounds*swb,dmgb));
std::stringstream str;
if (P3 > 0.99) {
str << _("(both should survive)") << EMPTY_COLUMN;
} else {
str << _("% Pr[kills/killed by/both survive]")
<< EMPTY_COLUMN << (int)(P1*100+0.5)
<< '/' << (int)((1-P1-P3)*100+0.5)
<< '/' << (int)(P3*100+0.5);
}
strings->attack_calculations.push_back(str.str());
}
LOG_NG << "done...\n";
return res;
}
battle_context::battle_context(const gamemap& map, const std::vector<team>& teams, const std::map<gamemap::location,unit>& units,
const gamestatus& status, const game_data& gamedata,
const gamemap::location& attacker_loc, const gamemap::location& defender_loc,
@ -958,10 +455,16 @@ unsigned int battle_context::rate_attacker_weapon(double attack_weight) const
// Bias towards more damaging attacks.
attack_weight += attacker_stats_->num_blows * attacker_stats_->damage;
if (attacker_stats_->poisons)
attack_weight += game_config::poison_amount;
attack_weight *= attacker_stats_->num_blows * attacker_stats_->damage;
if (defender_stats_->num_blows * defender_stats_->damage)
attack_weight /= defender_stats_->num_blows * defender_stats_->damage;
if (defender_stats_->num_blows * defender_stats_->damage) {
unsigned def_weight = defender_stats_->num_blows * defender_stats_->damage;
if (attacker_stats_->slows)
def_weight /= 2;
attack_weight /= def_weight;
}
return (unsigned int)(attack_weight * 100);
}

View file

@ -47,62 +47,8 @@ std::string recruit_unit(const gamemap& map, int team, unit_map& units,
unit u, gamemap::location& recruit_location,
display *disp=NULL, bool need_castle=true, bool full_movement=false);
//a structure which defines all the statistics for a potential
//battle that could take place.
struct battle_stats
{
int attacker_hp, defender_hp;
int chance_to_hit_attacker, chance_to_hit_defender;
int damage_attacker_takes, damage_defender_takes;
int amount_attacker_drains, amount_defender_drains;
int ndefends, nattacks;
int attack_with, defend_with;
bool attacker_plague, defender_plague;
std::string attacker_plague_type, defender_plague_type;
bool attacker_slows, defender_slows;
int rounds;
bool defender_strikes_first;
bool attacker_poisons, defender_poisons;
bool attacker_stones, defender_stones;
};
struct battle_stats_strings
{
std::string attack_name, defend_name;
std::string attack_type, defend_type;
std::string attack_special, defend_special;
std::string range;
std::string attack_icon, defend_icon;
std::vector<std::string> attack_calculations, defend_calculations;
};
//evaluate_battle_stats: a function which, if given an attacker
//and defender, and the number of a weapon to use, will report
//the statistics if that battle were to take place.
//
//attacker_terrain_override allows a different terrain to the
//one currently stood on by the attacker to be used in calculating
//the statistics. This is useful if one wants to look at the
//statistics if an attacker were to attack from one of several
//different locations.
//
//if include_strings is false, then none of the strings in
//battle_stats will be populated, and the function will run
//substantially faster.
battle_stats evaluate_battle_stats(const gamemap& map,
std::vector<team>& teams,
const gamemap::location& attacker,
const gamemap::location& defender,
int attack_with,
units_map& units,
const gamestatus& state,
const game_data& gamedata,
gamemap::TERRAIN attacker_terrain_override = 0,
battle_stats_strings *strings = NULL);
/* The battle_context class computes the statistics of a battle between an
* attacker and a defender unit. It is meant as a replacement of battle_stats.
* attacker and a defender unit.
*/
class battle_context
{

View file

@ -96,26 +96,10 @@ protected:
}
int choose_weapon(const location& attacker, const location& defender) {
const unit_map::const_iterator att = get_info().units.find(attacker);
wassert(att != get_info().units.end());
const std::vector<attack_type>& attacks = att->second.attacks();
double best_attack_rating = 0.0;
int best_attack = -1;
for(size_t n = 0; n != attacks.size(); ++n) {
if (attacks[n].attack_weight() > 0){
const battle_stats stats = evaluate_battle_stats(get_info().map, get_info().teams, attacker, defender, n, get_info().units, get_info().state, get_info().gameinfo);
const double attack_rating = stats.damage_defender_takes
*stats.nattacks*stats.chance_to_hit_defender*attacks[n].attack_weight();
if(best_attack == -1 || attack_rating > best_attack_rating) {
best_attack = n;
best_attack_rating = attack_rating;
}
}
}
return best_attack;
std::vector<battle_context> bc_vector;
return best_attack_weapon(get_info().map, get_info().teams,
get_info().units, get_info().state,
get_info().gameinfo, attacker, defender, bc_vector);
}
void get_villages() {
@ -593,8 +577,9 @@ gamemap::location ai::move_unit(location from, location to, std::map<location,pa
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_stats stats;
const int weapon = choose_weapon(res,itor->first,stats,0);
std::vector<battle_context> bc_vector;
const int weapon = best_attack_weapon(map_, teams_, units_, state_,
gameinfo_, res, *adj_i, bc_vector);
attack_enemy(res,itor->first,weapon);
break;
}
@ -1373,34 +1358,6 @@ bool ai::move_to_targets(std::map<gamemap::location,paths>& possible_moves, move
LOG_AI << "move: " << move.first << " -> " << move.second << '\n';
//search to see if there are any enemy units next
//to the tile which really should be attacked once the move is done.
gamemap::location adj[6];
get_adjacent_tiles(move.second,adj);
battle_stats bat_stats;
gamemap::location target;
int weapon = -1;
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()) {
const int res = choose_weapon(move.first,adj[n],bat_stats,
map_[move.second.x][move.second.y]);
//current behavior is to only make risk-free attacks
if(bat_stats.damage_attacker_takes == 0) {
weapon = res;
target = adj[n];
break;
}
}
}
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
@ -1410,12 +1367,35 @@ bool ai::move_to_targets(std::map<gamemap::location,paths>& possible_moves, move
return true;
}
const unit_map::const_iterator u_it = units_.find(move.second);
const unit_map::const_iterator un_it = units_.find(arrived_at);
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;
//if we're going to attack someone
if(u_it != units_.end() && !u_it->second.incapacitated() && weapon != -1) {
attack_enemy(move.second,target,weapon);
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()) {
std::vector<battle_context> bc_vector;
// FIXME: Must inform this we are cautious for weapon selection.
const int res = best_attack_weapon(map_, teams_, units_, state_, gameinfo_,
arrived_at, adj[n], bc_vector);
//current behavior is to only make risk-free attacks
if (bc_vector[res].get_defender_stats().damage == 0) {
attack_enemy(arrived_at,adj[n],res);
break;
}
}
}
}
//don't allow any other units to move onto the tile our unit

View file

@ -26,9 +26,6 @@ public:
virtual void play_turn();
virtual int choose_weapon(const location& att, const location& def,
battle_stats& cur_stats, gamemap::TERRAIN terrain, bool use_cache=false);
struct target {
enum TYPE { VILLAGE, LEADER, EXPLICIT, THREAT, BATTLE_AID, MASS, SUPPORT };
@ -106,9 +103,12 @@ protected:
struct attack_analysis
{
void analyze(const gamemap& map, std::map<location,unit>& units, int sims,
class ai& ai_obj, const move_map& dstsrc, const move_map& srcdst,
const move_map& enemy_dstsrc);
void analyze(const gamemap& map, unit_map& units,
const std::vector<team>& teams,
const gamestatus& status, const game_data& gamedata,
ai& ai_obj,
const ai::move_map& dstsrc, const ai::move_map& srcdst,
const ai::move_map& enemy_dstsrc);
double rating(double aggression, class ai& ai_obj) const;

View file

@ -14,6 +14,7 @@
#include "global.hpp"
#include "ai.hpp"
#include "attack_prediction.hpp"
#include "game_config.hpp"
#include "gamestatus.hpp"
#include "wassert.hpp"
@ -227,7 +228,7 @@ void ai::do_attack_analysis(
cur_analysis.is_surrounded = is_surrounded;
cur_analysis.analyze(map_, units_, 50, *this, dstsrc, srcdst, enemy_dstsrc);
cur_analysis.analyze(map_, units_, teams_, state_, gameinfo_, *this, dstsrc, srcdst, enemy_dstsrc);
if(cur_analysis.rating(current_team().aggression(),*this) > rating_to_beat) {
@ -260,7 +261,6 @@ struct battle_type {
const gamemap::location defender;
const gamemap::TERRAIN terrain;
int weapon;
battle_stats stats;
};
bool operator<(const battle_type& a, const battle_type& b)
@ -279,83 +279,10 @@ bool operator==(const battle_type& a, const battle_type& b)
std::set<battle_type> weapon_choice_cache;
int ai::choose_weapon(const location& att, const location& def,
battle_stats& cur_stats, gamemap::TERRAIN terrain, bool use_cache)
{
const std::map<location,unit>::const_iterator itor = units_.find(att);
if(itor == units_.end())
return -1;
static int cache_hits = 0;
static int cache_misses = 0;
if(use_cache == false) {
weapon_choice_cache.clear();
}
battle_type battle(att,def,terrain);
const std::set<battle_type>::const_iterator cache_itor = weapon_choice_cache.find(battle);
if(cache_itor != weapon_choice_cache.end()) {
wassert(*cache_itor == battle);
++cache_hits;
cur_stats = cache_itor->stats;
if(!(size_t(cache_itor->weapon) < itor->second.attacks().size())) {
LOG_STREAM(err, ai) << "cached illegal weapon: " << cache_itor->weapon
<< "/" << itor->second.attacks().size() << "\n";
}
wassert(size_t(cache_itor->weapon) < itor->second.attacks().size());
return cache_itor->weapon;
}
++cache_misses;
if((cache_misses%100) == 0) {
LOG_AI << "cache_stats: " << cache_hits << ":" << cache_misses << " " << weapon_choice_cache.size() << "\n";
}
int current_choice = -1;
double current_rating = 0.0;
const std::vector<attack_type>& attacks = itor->second.attacks();
wassert(!attacks.empty());
const unit_map::const_iterator d_itor = units_.find(def);
int d_hitpoints = d_itor->second.hitpoints();
int a_hitpoints = itor->second.hitpoints();
for(size_t a = 0; a != attacks.size(); ++a) {
if (attacks[a].attack_weight() > 0){
const battle_stats stats = evaluate_battle_stats(map_, teams_, att, def, a, units_,
state_, gameinfo_, terrain);
//TODO: improve this rating formula!
const double rating =
(double(stats.chance_to_hit_defender)/100.0)*
minimum<int>(stats.damage_defender_takes,d_hitpoints)*stats.nattacks *
attacks[a].attack_weight() -
(double(stats.chance_to_hit_attacker)/100.0)*
minimum<int>(stats.damage_attacker_takes,a_hitpoints)*stats.ndefends;
if(rating > current_rating || current_choice == -1) {
current_choice = a;
current_rating = rating;
cur_stats = stats;
}
}
}
wassert(size_t(current_choice) < attacks.size());
battle.stats = cur_stats;
battle.weapon = current_choice;
weapon_choice_cache.insert(battle);
return current_choice;
}
void ai::attack_analysis::analyze(const gamemap& map, unit_map& units, int num_sims, ai& ai_obj,
void ai::attack_analysis::analyze(const gamemap& map, unit_map& units,
const std::vector<team>& teams,
const gamestatus& status, const game_data& gamedata,
ai& ai_obj,
const ai::move_map& dstsrc, const ai::move_map& srcdst,
const ai::move_map& enemy_dstsrc)
{
@ -381,210 +308,125 @@ void ai::attack_analysis::analyze(const gamemap& map, unit_map& units, int num_s
double(defend_it->second.max_experience()))*target_value;
target_starting_damage = defend_it->second.max_hitpoints() -
defend_it->second.hitpoints();
chance_to_kill = 0.0;
avg_damage_inflicted = 0.0;
avg_damage_taken = 0.0;
resources_used = 0.0;
terrain_quality = 0.0;
avg_losses = 0.0;
const int target_max_hp = defend_it->second.max_hitpoints();
const int target_hp = defend_it->second.hitpoints();
static std::vector<int> hitpoints;
static std::vector<battle_stats> stats;
hitpoints.clear();
stats.clear();
weapons.clear();
std::vector<std::pair<location,location> >::const_iterator m;
for(m = movements.begin(); m != movements.end(); ++m) {
battle_stats bat_stats;
const int weapon = ai_obj.choose_weapon(m->first,target, bat_stats, map[m->second.x][m->second.y],true);
wassert(weapon != -1);
weapons.push_back(weapon);
stats.push_back(bat_stats);
hitpoints.push_back(units.find(m->first)->second.hitpoints());
}
for(int j = 0; j != num_sims; ++j) {
int defenderxp = 0;
bool defender_slowed = utils::string_bool(defend_it->second.get_state("slowed"));
int defhp = target_hp;
for(size_t i = 0; i != movements.size() && defhp; ++i) {
battle_stats& stat = stats[i];
int atthp = hitpoints[i];
int attacks = stat.nattacks;
int defends = stat.ndefends;
unit_map::const_iterator att = units.find(movements[i].first);
double cost = att->second.cost();
if(att->second.can_recruit()) {
uses_leader = true;
}
const bool on_village = map.is_village(movements[i].second);
bool attacker_slowed = utils::string_bool(att->second.get_state("slowed"));
//up to double the value of a unit based on experience
cost += (double(att->second.experience())/
double(att->second.max_experience()))*cost;
terrain_quality += (double(stat.chance_to_hit_attacker)/100.0)*cost * (on_village ? 0.5 : 1.0);
resources_used += cost;
bool defender_strikes_first = stat.defender_strikes_first;
while(attacks || defends) {
if(attacks && !defender_strikes_first) {
const int roll = rand()%100;
if(roll < stat.chance_to_hit_defender) {
defhp -= stat.damage_defender_takes;
atthp += stat.amount_attacker_drains;
if(defhp <= 0) {
defhp = 0;
break;
}
if(atthp > hitpoints[i]) {
atthp = hitpoints[i];
}
if(stat.attacker_slows && !defender_slowed) {
defender_slowed = true;
stat.damage_defender_takes = round_damage(stat.damage_defender_takes,1,2);
stat.amount_attacker_drains = round_damage(stat.amount_attacker_drains,1,2);
}
}
--attacks;
}
defender_strikes_first = false;
if(defends) {
const int roll = rand()%100;
if(roll < stat.chance_to_hit_attacker) {
atthp -= stat.damage_attacker_takes;
defhp += stat.amount_defender_drains;
if(atthp <= 0) {
atthp = 0;
//penalty for allowing plague is a 'negative' kill
if(stat.defender_plague) {
chance_to_kill -= 1.0;
}
break;
}
if(defhp > target_max_hp) {
defhp = target_max_hp;
}
if(stat.defender_slows && !attacker_slowed) {
attacker_slowed = true;
stat.damage_attacker_takes = round_damage(stat.damage_attacker_takes,1,2);
stat.amount_defender_drains = round_damage(stat.amount_defender_drains,1,2);
}
}
--defends;
}
}
const int xp = defend_it->second.level() * (defhp <= 0 ? game_config::kill_experience : 1);
const int xp_for_advance = att->second.max_experience() - att->second.experience();
//the reward for advancing a unit is to get a 'negative' loss of that unit
if(att->second.advances_to().empty() == false) {
if(xp >= xp_for_advance) {
avg_losses -= att->second.cost();
//ignore any damage done to this unit
atthp = hitpoints[i];
} else {
//the reward for getting a unit closer to advancement is to get
//the proportion of remaining experience needed, and multiply
//it by a quarter of the unit cost. This will cause the AI
//to heavily favor getting xp for close-to-advance units.
avg_losses -= (att->second.cost()*xp)/(xp_for_advance*4);
}
}
if(defhp <= 0) {
//the reward for killing with a unit that
//plagues is to get a 'negative' loss of that unit
if(stat.attacker_plague) {
avg_losses -= att->second.cost();
}
break;
} else if(atthp == 0) {
avg_losses += cost;
}
//if the attacker moved onto a village, reward it for doing so
else if(on_village) {
atthp += game_config::poison_amount*2; //double reward to emphasize getting onto villages
}
defenderxp += (atthp == 0 ? game_config::kill_experience:1)*att->second.level();
avg_damage_taken += hitpoints[i] - atthp;
}
//penalty for allowing advancement is a 'negative' kill, and
//defender's hitpoints get restored to maximum
if(defend_it->second.advances_to().empty() == false &&
defend_it->second.experience() < defend_it->second.max_experience() &&
defend_it->second.experience() + defenderxp >=
defend_it->second.max_experience()) {
chance_to_kill -= 1.0;
defhp = defend_it->second.hitpoints();
} else if(defhp == 0) {
chance_to_kill += 1.0;
} else if(map.gives_healing(defend_it->first)) {
defhp += map.gives_healing(defend_it->first);
if(defhp > target_hp)
defhp = target_hp;
}
avg_damage_inflicted += target_hp - defhp;
}
combatant *prev_def = NULL;
//calculate the 'alternative_terrain_quality' -- the best possible defensive values
//the attacking units could hope to achieve if they didn't attack and moved somewhere.
//this is could for comparative purposes to see just how vulnerable the AI is
//making itself
alternative_terrain_quality = 0.0;
double cost_sum = 0.0;
for(size_t i = 0; i != movements.size(); ++i) {
const unit_map::const_iterator att = units.find(movements[i].first);
const double cost = att->second.cost();
cost_sum += cost;
alternative_terrain_quality += cost*ai_obj.best_defensive_position(att->first,dstsrc,srcdst,enemy_dstsrc).chance_to_hit;
alternative_terrain_quality += cost*ai_obj.best_defensive_position(movements[i].first,dstsrc,srcdst,enemy_dstsrc).chance_to_hit;
}
alternative_terrain_quality /= cost_sum*100;
chance_to_kill /= num_sims;
avg_damage_inflicted /= num_sims;
avg_damage_taken /= num_sims;
terrain_quality /= resources_used;
resources_used /= num_sims;
avg_losses /= num_sims;
avg_damage_inflicted = 0.0;
avg_damage_taken = 0.0;
resources_used = 0.0;
terrain_quality = 0.0;
avg_losses = 0.0;
chance_to_kill = 0.0;
weapons.clear();
if(uses_leader) {
leader_threat = false;
double def_avg_experience = 0.0;
double first_chance_kill = 0.0;
double prob_dead_already = 0.0;
wassert(!movements.empty());
std::vector<std::pair<location,location> >::const_iterator m;
for (m = movements.begin(); m != movements.end(); ++m) {
// We fix up units map to reflect what this would look like.
unit_map::iterator att_it = units.find(m->first);
unit att_u = att_it->second;
units.erase(att_it);
units.insert(std::pair<location,unit>(m->second, att_u));
if (att_u.can_recruit()) {
uses_leader = true;
leader_threat = false;
}
std::vector<battle_context> bc_vector;
int weapon = best_attack_weapon(map, teams, units, status, gamedata, m->second,
target, bc_vector);
wassert(weapon != -1);
weapons.push_back(weapon);
combatant att(bc_vector[weapon].get_attacker_stats());
combatant *def = new combatant(bc_vector[weapon].get_defender_stats(), prev_def);
delete prev_def;
prev_def = def;
att.fight(*def);
double prob_killed = def->hp_dist[0] - prob_dead_already;
prob_dead_already = def->hp_dist[0];
avg_damage_taken += att_u.hitpoints() - att.average_hp();
double cost = att_u.cost();
const bool on_village = map.is_village(m->second);
//up to double the value of a unit based on experience
cost += (double(att_u.experience())/double(att_u.max_experience()))*cost;
resources_used += cost;
avg_losses += cost * att.hp_dist[0];
//double reward to emphasize getting onto villages
if (on_village) {
avg_damage_taken -= game_config::poison_amount*2;
}
terrain_quality += (double(bc_vector[weapon].get_defender_stats().chance_to_hit)/100.0)*cost * (on_village ? 0.5 : 1.0);
//the reward for advancing a unit is to get a 'negative' loss of that unit
if (!att_u.advances_to().empty()) {
int xp_for_advance = att_u.max_experience() - att_u.experience();
if (defend_it->second.level() > xp_for_advance)
avg_losses -= att_u.cost() * (1.0 - prob_dead_already);
else if (defend_it->second.level() * game_config::kill_experience > xp_for_advance)
avg_losses -= att_u.cost() * prob_killed;
//the reward for getting a unit closer to advancement is to get
//the proportion of remaining experience needed, and multiply
//it by a quarter of the unit cost. This will cause the AI
//to heavily favor getting xp for close-to-advance units.
avg_losses -= (att_u.cost()*defend_it->second.level())/(xp_for_advance*4);
//the reward for killing with a unit that
//plagues is to get a 'negative' loss of that unit
if (bc_vector[weapon].get_attacker_stats().plagues) {
avg_losses -= prob_killed * att_u.cost();
}
}
// FIXME: attack_prediction.cpp should understand advancement directly.
// For each level of attacker def gets 1 xp or kill_experience.
def_avg_experience += att_u.level() *
(1.0 - att.hp_dist[0] + game_config::kill_experience * att.hp_dist[0]);
if (m == movements.begin()) {
first_chance_kill = def->hp_dist[0];
}
}
if (!defend_it->second.advances_to().empty() &&
def_avg_experience >= defend_it->second.max_experience() - defend_it->second.experience()) {
// It's likely to advance: only if we can kill with first blow.
chance_to_kill = first_chance_kill;
// Negative average damage (it will advance).
avg_damage_inflicted = defend_it->second.hitpoints() - defend_it->second.max_hitpoints();
} else {
chance_to_kill = prev_def->hp_dist[0];
avg_damage_inflicted = defend_it->second.hitpoints() - prev_def->average_hp();
}
// Restore the units to their original positions.
for (m = movements.begin(); m != movements.end(); ++m) {
unit_map::iterator att_it = units.find(m->second);
unit att_u = att_it->second;
units.erase(att_it);
units.insert(std::pair<location,unit>(m->first, att_u));
}
}