The Battle for Wesnoth  1.13.4+dev
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
attack.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2016 by David White <[email protected]>
3  Part of the Battle for Wesnoth Project http://www.wesnoth.org/
4 
5  This program is free software; you can redistribute it and/or modify
6  it under the terms of the GNU General Public License as published by
7  the Free Software Foundation; either version 2 of the License, or
8  (at your option) any later version.
9  This program is distributed in the hope that it will be useful,
10  but WITHOUT ANY WARRANTY.
11 
12  See the COPYING file for more details.
13 */
14 
15 /**
16  * @file
17  * Calculate & analyze attacks of the default ai
18  */
19 
20 #include "global.hpp"
21 
22 #include "ai/manager.hpp"
23 #include "ai/default/contexts.hpp"
24 
25 #include "actions/attack.hpp"
26 #include "attack_prediction.hpp"
27 #include "game_config.hpp"
28 #include "log.hpp"
29 #include "map/map.hpp"
30 #include "team.hpp"
31 #include "units/unit.hpp"
32 #include "formula/callable_objects.hpp" // for location_callable
33 
34 static lg::log_domain log_ai("ai/attack");
35 #define LOG_AI LOG_STREAM(info, log_ai)
36 #define ERR_AI LOG_STREAM(err, log_ai)
37 
38 namespace ai {
39 
41  const readonly_context& ai_obj,
42  const move_map& dstsrc, const move_map& srcdst,
43  const move_map& enemy_dstsrc, double aggression)
44 {
45  const unit_map::const_iterator defend_it = units.find(target);
46  assert(defend_it != units.end());
47 
48  // See if the target is a threat to our leader or an ally's leader.
49  map_location adj[6];
51  size_t tile;
52  for(tile = 0; tile != 6; ++tile) {
53  const unit_map::const_iterator leader = units.find(adj[tile]);
54  if(leader != units.end() && leader->can_recruit() && !ai_obj.current_team().is_enemy(leader->side())) {
55  break;
56  }
57  }
58 
59  leader_threat = (tile != 6);
60  uses_leader = false;
61 
62  target_value = defend_it->cost();
63  target_value += (double(defend_it->experience())/
64  double(defend_it->max_experience()))*target_value;
65  target_starting_damage = defend_it->max_hitpoints() -
66  defend_it->hitpoints();
67 
68  // Calculate the 'alternative_terrain_quality' -- the best possible defensive values
69  // the attacking units could hope to achieve if they didn't attack and moved somewhere.
70  // This is used for comparative purposes, to see just how vulnerable the AI is
71  // making itself.
73  double cost_sum = 0.0;
74  for(size_t i = 0; i != movements.size(); ++i) {
75  const unit_map::const_iterator att = units.find(movements[i].first);
76  const double cost = att->cost();
77  cost_sum += cost;
78  alternative_terrain_quality += cost*ai_obj.best_defensive_position(movements[i].first,dstsrc,srcdst,enemy_dstsrc).chance_to_hit;
79  }
80  alternative_terrain_quality /= cost_sum*100;
81 
83  avg_damage_taken = 0.0;
84  resources_used = 0.0;
85  terrain_quality = 0.0;
86  avg_losses = 0.0;
87  chance_to_kill = 0.0;
88 
89  double def_avg_experience = 0.0;
90  double first_chance_kill = 0.0;
91 
92  double prob_dead_already = 0.0;
93  assert(!movements.empty());
94  std::vector<std::pair<map_location,map_location> >::const_iterator m;
95 
96  battle_context *prev_bc = nullptr;
97  const combatant *prev_def = nullptr;
98 
99  for (m = movements.begin(); m != movements.end(); ++m) {
100  // We fix up units map to reflect what this would look like.
101  unit_ptr up = units.extract(m->first);
102  up->set_location(m->second);
103  units.insert(up);
104  double m_aggression = aggression;
105 
106  if (up->can_recruit()) {
107  uses_leader = true;
108  // FIXME: suokko's r29531 omitted this line
109  leader_threat = false;
110  m_aggression = ai_obj.get_leader_aggression();
111  }
112 
113  bool from_cache = false;
114  battle_context *bc;
115 
116  // This cache is only about 99% correct, but speeds up evaluation by about 1000 times.
117  // We recalculate when we actually attack.
118  const readonly_context::unit_stats_cache_t::key_type cache_key = std::make_pair(target, &up->type());
119  const readonly_context::unit_stats_cache_t::iterator usc = ai_obj.unit_stats_cache().find(cache_key);
120  // Just check this attack is valid for this attacking unit (may be modified)
121  if (usc != ai_obj.unit_stats_cache().end() &&
122  usc->second.first.attack_num <
123  static_cast<int>(up->attacks().size())) {
124 
125  from_cache = true;
126  bc = new battle_context(usc->second.first, usc->second.second);
127  } else {
128  bc = new battle_context(units, m->second, target, -1, -1, m_aggression, prev_def);
129  }
130  const combatant &att = bc->get_attacker_combatant(prev_def);
131  const combatant &def = bc->get_defender_combatant(prev_def);
132 
133  delete prev_bc;
134  prev_bc = bc;
135  prev_def = &bc->get_defender_combatant(prev_def);
136 
137  if ( !from_cache ) {
138  ai_obj.unit_stats_cache().insert(
139  std::make_pair(cache_key, std::make_pair(bc->get_attacker_stats(),
140  bc->get_defender_stats())));
141  }
142 
143  // Note we didn't fight at all if defender is already dead.
144  double prob_fought = (1.0 - prob_dead_already);
145 
146  /** @todo 1.9 add combatant.prob_killed */
147  double prob_killed = def.hp_dist[0] - prob_dead_already;
148  prob_dead_already = def.hp_dist[0];
149 
150  double prob_died = att.hp_dist[0];
151  double prob_survived = (1.0 - prob_died) * prob_fought;
152 
153  double cost = up->cost();
154  const bool on_village = map.is_village(m->second);
155  // Up to double the value of a unit based on experience
156  cost += (double(up->experience()) / up->max_experience())*cost;
157  resources_used += cost;
158  avg_losses += cost * prob_died;
159 
160  // add half of cost for poisoned unit so it might get chance to heal
161  avg_losses += cost * up->get_state(unit::STATE_POISONED) /2;
162 
163  if (!bc->get_defender_stats().is_poisoned) {
165  }
166 
167  // Double reward to emphasize getting onto villages if they survive.
168  if (on_village) {
169  avg_damage_taken -= game_config::poison_amount*2 * prob_survived;
170  }
171 
172  terrain_quality += (double(bc->get_defender_stats().chance_to_hit)/100.0)*cost * (on_village ? 0.5 : 1.0);
173 
174  double advance_prob = 0.0;
175  // The reward for advancing a unit is to get a 'negative' loss of that unit
176  if (!up->advances_to().empty()) {
177  int xp_for_advance = up->max_experience() - up->experience();
178 
179  // See bug #6272... in some cases, unit already has got enough xp to advance,
180  // but hasn't (bug elsewhere?). Can cause divide by zero.
181  if (xp_for_advance <= 0)
182  xp_for_advance = 1;
183 
184  int fight_xp = defend_it->level();
185  int kill_xp = game_config::kill_xp(fight_xp);
186 
187  if (fight_xp >= xp_for_advance) {
188  advance_prob = prob_fought;
189  avg_losses -= up->cost() * prob_fought;
190  } else if (kill_xp >= xp_for_advance) {
191  advance_prob = prob_killed;
192  avg_losses -= up->cost() * prob_killed;
193  // The reward for getting a unit closer to advancement
194  // (if it didn't advance) is to get the proportion of
195  // remaining experience needed, and multiply it by
196  // a quarter of the unit cost.
197  // This will cause the AI to heavily favor
198  // getting xp for close-to-advance units.
199  avg_losses -= up->cost() * 0.25 *
200  fight_xp * (prob_fought - prob_killed)
201  / xp_for_advance;
202  } else {
203  avg_losses -= up->cost() * 0.25 *
204  (kill_xp * prob_killed + fight_xp * (prob_fought - prob_killed))
205  / xp_for_advance;
206  }
207 
208  // The reward for killing with a unit that plagues
209  // is to get a 'negative' loss of that unit.
210  if (bc->get_attacker_stats().plagues) {
211  avg_losses -= prob_killed * up->cost();
212  }
213  }
214 
215  // If we didn't advance, we took this damage.
216  avg_damage_taken += (up->hitpoints() - att.average_hp()) * (1.0 - advance_prob);
217 
218  /**
219  * @todo 1.9: attack_prediction.cpp should understand advancement
220  * directly. For each level of attacker def gets 1 xp or
221  * kill_experience.
222  */
223  int fight_xp = up->level();
224  int kill_xp = game_config::kill_xp(fight_xp);
225  def_avg_experience += fight_xp * (1.0 - att.hp_dist[0]) + kill_xp * att.hp_dist[0];
226  if (m == movements.begin()) {
227  first_chance_kill = def.hp_dist[0];
228  }
229  }
230 
231  if (!defend_it->advances_to().empty() &&
232  def_avg_experience >= defend_it->max_experience() - defend_it->experience()) {
233  // It's likely to advance: only if we can kill with first blow.
234  chance_to_kill = first_chance_kill;
235  // Negative average damage (it will advance).
236  avg_damage_inflicted += defend_it->hitpoints() - defend_it->max_hitpoints();
237  } else {
238  chance_to_kill = prev_def->hp_dist[0];
239  avg_damage_inflicted += defend_it->hitpoints() - prev_def->average_hp(map.gives_healing(defend_it->get_location()));
240  }
241 
242  delete prev_bc;
244 
245  // Restore the units to their original positions.
246  for (m = movements.begin(); m != movements.end(); ++m) {
247  units.move(m->second, m->first);
248  }
249 }
250 
252 {
253  std::set<map_location> &attacks = manager::get_ai_info().recent_attacks;
254  for(std::set<map_location>::const_iterator i = attacks.begin(); i != attacks.end(); ++i) {
255  if(distance_between(*i,loc) < 4) {
256  return true;
257  }
258  }
259 
260  return false;
261 }
262 
263 
264 double attack_analysis::rating(double aggression, const readonly_context& ai_obj) const
265 {
266  if(leader_threat) {
267  aggression = 1.0;
268  }
269 
270  if(uses_leader) {
271  aggression = ai_obj.get_leader_aggression();
272  }
273 
274  double value = chance_to_kill*target_value - avg_losses*(1.0-aggression);
275 
277  // This situation looks like it might be a bad move:
278  // we are moving our attackers out of their optimal terrain
279  // into sub-optimal terrain.
280  // Calculate the 'exposure' of our units to risk.
281 
282 #ifdef SUOKKO
283  //FIXME: this code was in sukko's r29531 Correct?
284  const double exposure_mod = uses_leader ? ai_obj.current_team().caution()* 8.0 : ai_obj.current_team().caution() * 4.0;
285  const double exposure = exposure_mod*resources_used*((terrain_quality - alternative_terrain_quality)/10)*vulnerability/std::max<double>(0.01,support);
286 #else
287  const double exposure_mod = uses_leader ? 2.0 : ai_obj.get_caution();
288  const double exposure = exposure_mod*resources_used*(terrain_quality - alternative_terrain_quality)*vulnerability/std::max<double>(0.01,support);
289 #endif
290  LOG_AI << "attack option has base value " << value << " with exposure " << exposure << ": "
291  << vulnerability << "/" << support << " = " << (vulnerability/std::max<double>(support,0.1)) << "\n";
292  value -= exposure*(1.0-aggression);
293  }
294 
295  // Prefer to attack already damaged targets.
296  value += ((target_starting_damage/3 + avg_damage_inflicted) - (1.0-aggression)*avg_damage_taken)/10.0;
297 
298  // If the unit is surrounded and there is no support,
299  // or if the unit is surrounded and the average damage is 0,
300  // the unit skips its sanity check and tries to break free as good as possible.
301  if(!is_surrounded || (support != 0 && avg_damage_taken != 0))
302  {
303  // Sanity check: if we're putting ourselves at major risk,
304  // and have no chance to kill, and we're not aiding our allies
305  // who are also attacking, then don't do it.
306  if(vulnerability > 50.0 && vulnerability > support*2.0
307  && chance_to_kill < 0.02 && aggression < 0.75
308  && !attack_close(target)) {
309  return -1.0;
310  }
311  }
312 
315  }
316 
317  value /= ((resources_used/2) + (resources_used/2)*terrain_quality);
318 
319  if(leader_threat) {
320  value *= 5.0;
321  }
322 
323  LOG_AI << "attack on " << target << ": attackers: " << movements.size()
324  << " value: " << value << " chance to kill: " << chance_to_kill
325  << " damage inflicted: " << avg_damage_inflicted
326  << " damage taken: " << avg_damage_taken
327  << " vulnerability: " << vulnerability
328  << " support: " << support
329  << " quality: " << terrain_quality
330  << " alternative quality: " << alternative_terrain_quality << "\n";
331 
332  return value;
333 }
334 
336 {
337  using namespace game_logic;
338  if(key == "target") {
339  return variant(new location_callable(target));
340  } else if(key == "movements") {
341  std::vector<variant> res;
342  for(size_t n = 0; n != movements.size(); ++n) {
343  map_formula_callable* item = new map_formula_callable(nullptr);
344  item->add("src", variant(new location_callable(movements[n].first)));
345  item->add("dst", variant(new location_callable(movements[n].second)));
346  res.push_back(variant(item));
347  }
348 
349  return variant(&res);
350  } else if(key == "units") {
351  std::vector<variant> res;
352  for(size_t n = 0; n != movements.size(); ++n) {
353  res.push_back(variant(new location_callable(movements[n].first)));
354  }
355 
356  return variant(&res);
357  } else if(key == "target_value") {
358  return variant(static_cast<int>(target_value*1000));
359  } else if(key == "avg_losses") {
360  return variant(static_cast<int>(avg_losses*1000));
361  } else if(key == "chance_to_kill") {
362  return variant(static_cast<int>(chance_to_kill*100));
363  } else if(key == "avg_damage_inflicted") {
364  return variant(static_cast<int>(avg_damage_inflicted));
365  } else if(key == "target_starting_damage") {
367  } else if(key == "avg_damage_taken") {
368  return variant(static_cast<int>(avg_damage_taken));
369  } else if(key == "resources_used") {
370  return variant(static_cast<int>(resources_used));
371  } else if(key == "terrain_quality") {
372  return variant(static_cast<int>(terrain_quality));
373  } else if(key == "alternative_terrain_quality") {
374  return variant(static_cast<int>(alternative_terrain_quality));
375  } else if(key == "vulnerability") {
376  return variant(static_cast<int>(vulnerability));
377  } else if(key == "support") {
378  return variant(static_cast<int>(support));
379  } else if(key == "leader_threat") {
380  return variant(leader_threat);
381  } else if(key == "uses_leader") {
382  return variant(uses_leader);
383  } else if(key == "is_surrounded") {
384  return variant(is_surrounded);
385  } else {
386  return variant();
387  }
388 }
389 
390 void attack_analysis::get_inputs(std::vector<game_logic::formula_input>* inputs) const
391 {
392  using namespace game_logic;
393  inputs->push_back(formula_input("target", FORMULA_READ_ONLY));
394  inputs->push_back(formula_input("movements", FORMULA_READ_ONLY));
395  inputs->push_back(formula_input("units", FORMULA_READ_ONLY));
396  inputs->push_back(formula_input("target_value", FORMULA_READ_ONLY));
397  inputs->push_back(formula_input("avg_losses", FORMULA_READ_ONLY));
398  inputs->push_back(formula_input("chance_to_kill", FORMULA_READ_ONLY));
399  inputs->push_back(formula_input("avg_damage_inflicted", FORMULA_READ_ONLY));
400  inputs->push_back(formula_input("target_starting_damage", FORMULA_READ_ONLY));
401  inputs->push_back(formula_input("avg_damage_taken", FORMULA_READ_ONLY));
402  inputs->push_back(formula_input("resources_used", FORMULA_READ_ONLY));
403  inputs->push_back(formula_input("terrain_quality", FORMULA_READ_ONLY));
404  inputs->push_back(formula_input("alternative_terrain_quality", FORMULA_READ_ONLY));
405  inputs->push_back(formula_input("vulnerability", FORMULA_READ_ONLY));
406  inputs->push_back(formula_input("support", FORMULA_READ_ONLY));
407  inputs->push_back(formula_input("leader_threat", FORMULA_READ_ONLY));
408  inputs->push_back(formula_input("uses_leader", FORMULA_READ_ONLY));
409  inputs->push_back(formula_input("is_surrounded", FORMULA_READ_ONLY));
410 }
411 
412 } //end of namespace ai
int kill_xp(int level)
Definition: game_config.hpp:45
bool leader_threat
Is true if the unit is a threat to our leader.
Definition: contexts.hpp:137
double avg_damage_taken
The average hitpoints damage taken.
Definition: contexts.hpp:115
std::vector< double > hp_dist
Resulting probability distribution (might be not as large as max_hp)
unit_iterator end()
Definition: map.hpp:311
variant get_value(const std::string &key) const
Definition: attack.cpp:335
Various functions that implement attacks and attack calculations.
const combatant & get_attacker_combatant(const combatant *prev_def=nullptr)
Get the simulation results.
Definition: attack.cpp:410
double avg_damage_inflicted
The average hitpoints damage inflicted.
Definition: contexts.hpp:110
void get_adjacent_tiles(const map_location &a, map_location *res)
Function which, given a location, will place all adjacent locations in res.
Definition: location.hpp:274
bool is_village(const map_location &loc) const
Definition: map.cpp:68
bool is_enemy(int n) const
Definition: team.hpp:247
double vulnerability
The vulnerability is the power projection of enemy units onto the hex we're standing on...
Definition: contexts.hpp:134
double resources_used
The sum of the values of units used in the attack.
Definition: contexts.hpp:118
double chance_to_kill
Estimated % chance to kill the unit.
Definition: contexts.hpp:107
-file sdl_utils.hpp
unsigned int chance_to_hit
Effective chance to hit as a percentage (all factors accounted for).
Definition: attack.hpp:74
double terrain_quality
The weighted average of the % chance to hit each attacking unit.
Definition: contexts.hpp:121
double target_value
The value of the unit being targeted.
Definition: contexts.hpp:101
const combatant & get_defender_combatant(const combatant *prev_def=nullptr)
Definition: attack.cpp:422
std::multimap< map_location, map_location > move_map
The standard way in which a map of possible moves is recorded.
Definition: game_info.hpp:47
A small explanation about what's going on here: Each action has access to two game_info objects First...
Definition: actions.cpp:57
void analyze(const gamemap &map, unit_map &units, const readonly_context &ai_obj, const move_map &dstsrc, const move_map &srcdst, const move_map &enemy_dstsrc, double aggression)
Definition: attack.cpp:40
virtual unit_stats_cache_t & unit_stats_cache() const =0
std::pair< unit_iterator, bool > insert(unit_ptr p)
Adds the unit to the map.
Definition: map.cpp:126
size_t distance_between(const map_location &a, const map_location &b)
Function which gives the number of hexes between two tiles (i.e.
Definition: location.hpp:357
double alternative_terrain_quality
The weighted average of the % defense of the best possible terrain that the attacking units could rea...
Definition: contexts.hpp:128
void get_inputs(std::vector< game_logic::formula_input > *inputs) const
Definition: attack.cpp:390
GLsizei const GLfloat * value
Definition: glew.h:1817
bool attack_close(const map_location &loc) const
Definition: attack.cpp:251
const battle_context_unit_stats & get_defender_stats() const
This method returns the statistics of the defender.
Definition: attack.hpp:163
bool plagues
Attack turns opponent into a zombie when fatal.
Definition: attack.hpp:59
Encapsulates the map of the game.
Definition: map.hpp:37
Computes the statistics of a battle between an attacker and a defender unit.
Definition: attack.hpp:135
Managing the AIs lifecycle - headers.
static lg::log_domain log_ai("ai/attack")
std::vector< formula_input > inputs() const
Definition: callable.hpp:50
#define LOG_AI
Definition: attack.cpp:35
Encapsulates the map of the game.
Definition: location.hpp:38
bool uses_leader
Is true if this attack sequence makes use of the leader.
Definition: contexts.hpp:140
All combat-related info.
GLuint res
Definition: glew.h:9258
virtual const team & current_team() const =0
std::vector< std::pair< map_location, map_location > > movements
Definition: contexts.hpp:98
std::set< map_location > recent_attacks
hack.
Definition: game_info.hpp:125
Default AI contexts.
double avg_losses
The value on average, of units lost in the combat.
Definition: contexts.hpp:104
size_t i
Definition: function.cpp:1057
bool is_poisoned
True if the unit is poisoned at the beginning of the battle.
Definition: attack.hpp:54
map_formula_callable & add(const std::string &key, const variant &value)
Definition: formula.cpp:41
std::pair< unit_iterator, bool > move(const map_location &src, const map_location &dst)
Moves a unit from location src to location dst.
Definition: map.cpp:79
GLclampd n
Definition: glew.h:5903
const GLdouble * m
Definition: glew.h:6968
virtual defensive_position const & best_defensive_position(const map_location &unit, const move_map &dstsrc, const move_map &srcdst, const move_map &enemy_dstsrc) const =0
static game_info & get_ai_info()
Gets global AI-game info.
Definition: manager.cpp:771
CALLABLE_WRAPPER_INPUT_END if(key=="terrain")
unit_ptr extract(const map_location &loc)
Extracts a unit from the map.
Definition: map.cpp:249
Standard logging facilities (interface).
GLint * first
Definition: glew.h:1496
virtual double get_leader_aggression() const =0
Container associating units to locations.
Definition: map.hpp:90
bool is_surrounded
Is true if the units involved in this attack sequence are surrounded.
Definition: contexts.hpp:143
double average_hp(unsigned int healing=0) const
What's the average hp (weighted average of hp_dist).
const battle_context_unit_stats & get_attacker_stats() const
This method returns the statistics of the attacker.
Definition: attack.hpp:160
unit_iterator find(size_t id)
Definition: map.cpp:285
std::string::const_iterator iterator
Definition: tokenizer.hpp:21
GLsizei const GLcharARB ** string
Definition: glew.h:4503
unit_map * units
Definition: resources.cpp:35
virtual double get_caution() const =0
double poisoned
Resulting chance we are poisoned.
double rating(double aggression, const readonly_context &ai_obj) const
Definition: attack.cpp:264
int gives_healing(const map_location &loc) const
Definition: map.cpp:70