The Battle for Wesnoth  1.17.21+dev
attack.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2023
3  by David White <dave@whitevine.net>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 /**
17  * @file
18  * Calculate & analyze attacks of the default ai
19  */
20 
21 #include "ai/manager.hpp"
22 #include "ai/default/contexts.hpp"
23 #include "ai/actions.hpp"
24 #include "ai/formula/ai.hpp"
26 
27 #include "actions/attack.hpp"
28 #include "attack_prediction.hpp"
29 #include "game_config.hpp"
30 #include "log.hpp"
31 #include "map/map.hpp"
32 #include "team.hpp"
33 #include "units/unit.hpp"
34 #include "formula/callable_objects.hpp" // for location_callable
35 #include "resources.hpp"
36 #include "game_board.hpp"
37 
38 static lg::log_domain log_ai("ai/attack");
39 #define LOG_AI LOG_STREAM(info, log_ai)
40 #define ERR_AI LOG_STREAM(err, log_ai)
41 
42 namespace ai {
43 
44 extern ai_context& get_ai_context(wfl::const_formula_callable_ptr for_fai);
45 
46 void attack_analysis::analyze(const gamemap& map, unit_map& units,
47  const readonly_context& ai_obj,
48  const move_map& dstsrc, const move_map& srcdst,
49  const move_map& enemy_dstsrc, double aggression)
50 {
51  const unit_map::const_iterator defend_it = units.find(target);
52  assert(defend_it != units.end());
53 
54  // See if the target is a threat to our leader or an ally's leader.
55  const auto adj = get_adjacent_tiles(target);
56  std::size_t tile;
57  for(tile = 0; tile < adj.size(); ++tile) {
58  const unit_map::const_iterator leader = units.find(adj[tile]);
59  if(leader != units.end() && leader->can_recruit() && !ai_obj.current_team().is_enemy(leader->side())) {
60  break;
61  }
62  }
63 
64  leader_threat = (tile != 6);
65  uses_leader = false;
66 
67  target_value = defend_it->cost();
68  target_value += (static_cast<double>(defend_it->experience())/
69  static_cast<double>(defend_it->max_experience()))*target_value;
70  target_starting_damage = defend_it->max_hitpoints() -
71  defend_it->hitpoints();
72 
73  // Calculate the 'alternative_terrain_quality' -- the best possible defensive values
74  // the attacking units could hope to achieve if they didn't attack and moved somewhere.
75  // This is used for comparative purposes, to see just how vulnerable the AI is
76  // making itself.
78  double cost_sum = 0.0;
79  for(std::size_t i = 0; i != movements.size(); ++i) {
80  const unit_map::const_iterator att = units.find(movements[i].first);
81  const double cost = att->cost();
82  cost_sum += cost;
83  alternative_terrain_quality += cost*ai_obj.best_defensive_position(movements[i].first,dstsrc,srcdst,enemy_dstsrc).chance_to_hit;
84  }
85  alternative_terrain_quality /= cost_sum*100;
86 
88  avg_damage_taken = 0.0;
89  resources_used = 0.0;
90  terrain_quality = 0.0;
91  avg_losses = 0.0;
92  chance_to_kill = 0.0;
93 
94  double def_avg_experience = 0.0;
95  double first_chance_kill = 0.0;
96 
97  double prob_dead_already = 0.0;
98  assert(!movements.empty());
99  std::vector<std::pair<map_location,map_location>>::const_iterator m;
100 
101  std::unique_ptr<battle_context> bc(nullptr);
102  std::unique_ptr<battle_context> old_bc(nullptr);
103 
104  const combatant *prev_def = nullptr;
105 
106  for (m = movements.begin(); m != movements.end(); ++m) {
107  // We fix up units map to reflect what this would look like.
108  unit_ptr up = units.extract(m->first);
109  up->set_location(m->second);
110  units.insert(up);
111  double m_aggression = aggression;
112 
113  if (up->can_recruit()) {
114  uses_leader = true;
115  // FIXME: suokko's r29531 omitted this line
116  leader_threat = false;
117  m_aggression = ai_obj.get_leader_aggression();
118  }
119 
120  bool from_cache = false;
121 
122  // Swap the two context pointers. old_bc should be null at this point, so bc is cleared
123  // and old_bc takes ownership of the context pointer. This allows prev_def to remain
124  // valid until it's reassigned.
125  old_bc.swap(bc);
126 
127  // This cache is only about 99% correct, but speeds up evaluation by about 1000 times.
128  // We recalculate when we actually attack.
129  const readonly_context::unit_stats_cache_t::key_type cache_key = std::pair(target, &up->type());
130  const readonly_context::unit_stats_cache_t::iterator usc = ai_obj.unit_stats_cache().find(cache_key);
131  // Just check this attack is valid for this attacking unit (may be modified)
132  if (usc != ai_obj.unit_stats_cache().end() &&
133  usc->second.first.attack_num <
134  static_cast<int>(up->attacks().size())) {
135 
136  from_cache = true;
137  bc.reset(new battle_context(usc->second.first, usc->second.second));
138  } else {
139  bc.reset(new battle_context(units, m->second, target, -1, -1, m_aggression, prev_def));
140  }
141  const combatant &att = bc->get_attacker_combatant(prev_def);
142  const combatant &def = bc->get_defender_combatant(prev_def);
143 
144  prev_def = &bc->get_defender_combatant(prev_def);
145 
146  // We no longer need the old context since prev_def has been reassigned.
147  old_bc.reset(nullptr);
148 
149  if ( !from_cache ) {
150  ai_obj.unit_stats_cache().emplace(cache_key, std::pair(
151  bc->get_attacker_stats(),
152  bc->get_defender_stats()
153  ));
154  }
155 
156  // Note we didn't fight at all if defender is already dead.
157  double prob_fought = (1.0 - prob_dead_already);
158 
159  double prob_killed = def.hp_dist[0] - prob_dead_already;
160  prob_dead_already = def.hp_dist[0];
161 
162  double prob_died = att.hp_dist[0];
163  double prob_survived = (1.0 - prob_died) * prob_fought;
164 
165  double cost = up->cost();
166  const bool on_village = map.is_village(m->second);
167  // Up to double the value of a unit based on experience
168  cost += (static_cast<double>(up->experience()) / up->max_experience())*cost;
169  resources_used += cost;
170  avg_losses += cost * prob_died;
171 
172  // add half of cost for poisoned unit so it might get chance to heal
173  avg_losses += cost * up->get_state(unit::STATE_POISONED) /2;
174 
175  if (!bc->get_defender_stats().is_poisoned) {
176  avg_damage_inflicted += game_config::poison_amount * 2 * bc->get_defender_combatant().poisoned * (1 - prob_killed);
177  }
178 
179  // Double reward to emphasize getting onto villages if they survive.
180  if (on_village) {
181  avg_damage_taken -= game_config::poison_amount*2 * prob_survived;
182  }
183 
184  terrain_quality += (static_cast<double>(bc->get_defender_stats().chance_to_hit)/100.0)*cost * (on_village ? 0.5 : 1.0);
185 
186  double advance_prob = 0.0;
187  // The reward for advancing a unit is to get a 'negative' loss of that unit
188  if (!up->advances_to().empty()) {
189  int xp_for_advance = up->experience_to_advance();
190 
191  // See bug #6272... in some cases, unit already has got enough xp to advance,
192  // but hasn't (bug elsewhere?). Can cause divide by zero.
193  if (xp_for_advance == 0)
194  xp_for_advance = 1;
195 
196  int fight_xp = game_config::combat_xp(defend_it->level());
197  int kill_xp = game_config::kill_xp(fight_xp);
198 
199  if (fight_xp >= xp_for_advance) {
200  advance_prob = prob_fought;
201  avg_losses -= up->cost() * prob_fought;
202  } else if (kill_xp >= xp_for_advance) {
203  advance_prob = prob_killed;
204  avg_losses -= up->cost() * prob_killed;
205  // The reward for getting a unit closer to advancement
206  // (if it didn't advance) is to get the proportion of
207  // remaining experience needed, and multiply it by
208  // a quarter of the unit cost.
209  // This will cause the AI to heavily favor
210  // getting xp for close-to-advance units.
211  avg_losses -= up->cost() * 0.25 *
212  fight_xp * (prob_fought - prob_killed)
213  / xp_for_advance;
214  } else {
215  avg_losses -= up->cost() * 0.25 *
216  (kill_xp * prob_killed + fight_xp * (prob_fought - prob_killed))
217  / xp_for_advance;
218  }
219 
220  // The reward for killing with a unit that plagues
221  // is to get a 'negative' loss of that unit.
222  if (bc->get_attacker_stats().plagues) {
223  avg_losses -= prob_killed * up->cost();
224  }
225  }
226 
227  // If we didn't advance, we took this damage.
228  avg_damage_taken += (up->hitpoints() - att.average_hp()) * (1.0 - advance_prob);
229 
230  int fight_xp = game_config::combat_xp(up->level());
231  int kill_xp = game_config::kill_xp(fight_xp);
232  def_avg_experience += fight_xp * (1.0 - att.hp_dist[0]) + kill_xp * att.hp_dist[0];
233  if (m == movements.begin()) {
234  first_chance_kill = def.hp_dist[0];
235  }
236  }
237 
238  if (!defend_it->advances_to().empty() &&
239  def_avg_experience >= defend_it->experience_to_advance()) {
240  // It's likely to advance: only if we can kill with first blow.
241  chance_to_kill = first_chance_kill;
242  // Negative average damage (it will advance).
243  avg_damage_inflicted += defend_it->hitpoints() - defend_it->max_hitpoints();
244  } else {
245  chance_to_kill = prev_def->hp_dist[0];
246  avg_damage_inflicted += defend_it->hitpoints() - prev_def->average_hp(map.gives_healing(defend_it->get_location()));
247  }
248 
250 
251  // Restore the units to their original positions.
252  for (m = movements.begin(); m != movements.end(); ++m) {
253  units.move(m->second, m->first);
254  }
255 }
256 
258 {
259  std::set<map_location> &attacks = manager::get_singleton().get_ai_info().recent_attacks;
260  for(std::set<map_location>::const_iterator i = attacks.begin(); i != attacks.end(); ++i) {
261  if(distance_between(*i,loc) < 4) {
262  return true;
263  }
264  }
265 
266  return false;
267 }
268 
269 
270 double attack_analysis::rating(double aggression, const readonly_context& ai_obj) const
271 {
272  if(leader_threat) {
273  aggression = 1.0;
274  }
275 
276  if(uses_leader) {
277  aggression = ai_obj.get_leader_aggression();
278  }
279 
280  double value = chance_to_kill*target_value - avg_losses*(1.0-aggression);
281 
283  // This situation looks like it might be a bad move:
284  // we are moving our attackers out of their optimal terrain
285  // into sub-optimal terrain.
286  // Calculate the 'exposure' of our units to risk.
287 
288  const double exposure_mod = uses_leader ? 2.0 : ai_obj.get_caution();
289  const double exposure = exposure_mod*resources_used*(terrain_quality - alternative_terrain_quality)*vulnerability/std::max<double>(0.01,support);
290  LOG_AI << "attack option has base value " << value << " with exposure " << exposure << ": "
291  << vulnerability << "/" << support << " = " << (vulnerability/std::max<double>(support,0.1));
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 
313  if(!leader_threat && vulnerability*terrain_quality > 0.0 && support != 0) {
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;
331 
332  return value;
333 }
334 
335 wfl::variant attack_analysis::get_value(const std::string& key) const
336 {
337  using namespace wfl;
338  if(key == "target") {
339  return variant(std::make_shared<location_callable>(target));
340  } else if(key == "movements") {
341  std::vector<variant> res;
342  for(std::size_t n = 0; n != movements.size(); ++n) {
343  auto item = std::make_shared<map_formula_callable>(nullptr);
344  item->add("src", variant(std::make_shared<location_callable>(movements[n].first)));
345  item->add("dst", variant(std::make_shared<location_callable>(movements[n].second)));
346  res.emplace_back(item);
347  }
348 
349  return variant(res);
350  } else if(key == "units") {
351  std::vector<variant> res;
352  for(std::size_t n = 0; n != movements.size(); ++n) {
353  res.emplace_back(std::make_shared<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 
391 {
392  add_input(inputs, "target");
393  add_input(inputs, "movements");
394  add_input(inputs, "units");
395  add_input(inputs, "target_value");
396  add_input(inputs, "avg_losses");
397  add_input(inputs, "chance_to_kill");
398  add_input(inputs, "avg_damage_inflicted");
399  add_input(inputs, "target_starting_damage");
400  add_input(inputs, "avg_damage_taken");
401  add_input(inputs, "resources_used");
402  add_input(inputs, "terrain_quality");
403  add_input(inputs, "alternative_terrain_quality");
404  add_input(inputs, "vulnerability");
405  add_input(inputs, "support");
406  add_input(inputs, "leader_threat");
407  add_input(inputs, "uses_leader");
408  add_input(inputs, "is_surrounded");
409 }
410 
412  //If we get an attack analysis back we will do the first attack.
413  //Then the AI can get run again and re-choose.
414  if(movements.empty()) {
415  return wfl::variant(false);
416  }
417 
418  unit_map& units = resources::gameboard->units();
419 
420  //make sure that unit which has to attack is at given position and is able to attack
421  unit_map::const_iterator unit = units.find(movements.front().first);
422  if(!unit.valid() || unit->attacks_left() == 0) {
423  return wfl::variant(false);
424  }
425 
426  const map_location& move_from = movements.front().first;
427  const map_location& att_src = movements.front().second;
428  const map_location& att_dst = target;
429 
430  //check if target is still valid
431  unit = units.find(att_dst);
432  if(unit == units.end()) {
433  return wfl::variant(std::make_shared<wfl::safe_call_result>(fake_ptr(), attack_result::E_EMPTY_DEFENDER, move_from));
434  }
435 
436  //check if we need to move
437  if(move_from != att_src) {
438  //now check if location to which we want to move is still unoccupied
439  unit = units.find(att_src);
440  if(unit != units.end()) {
441  return wfl::variant(std::make_shared<wfl::safe_call_result>(fake_ptr(), move_result::E_NO_UNIT, move_from));
442  }
443 
444  ai::move_result_ptr result = get_ai_context(ctxt.as_callable()).execute_move_action(move_from, att_src);
445  if(!result->is_ok()) {
446  //move part failed
447  LOG_AI << "ERROR #" << result->get_status() << " while executing 'attack' formula function";
448  return wfl::variant(std::make_shared<wfl::safe_call_result>(fake_ptr(), result->get_status(), result->get_unit_location()));
449  }
450  }
451 
452  if(units.count(att_src)) {
454  if(!result->is_ok()) {
455  //attack failed
456  LOG_AI << "ERROR #" << result->get_status() << " while executing 'attack' formula function";
457  return wfl::variant(std::make_shared<wfl::safe_call_result>(fake_ptr(), result->get_status()));
458  }
459  }
460  return wfl::variant(true);
461 }
462 
463 } //end of namespace ai
Various functions that implement attacks and attack calculations.
Managing the AI-Game interaction - AI actions and their results.
#define LOG_AI
Definition: attack.cpp:39
static lg::log_domain log_ai("ai/attack")
Managing the AIs lifecycle - headers TODO: Refactor history handling and internal commands.
std::vector< std::pair< map_location, map_location > > movements
Definition: contexts.hpp:75
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:46
bool uses_leader
Is true if this attack sequence makes use of the leader.
Definition: contexts.hpp:117
wfl::variant get_value(const std::string &key) const override
Definition: attack.cpp:335
void get_inputs(wfl::formula_input_vector &inputs) const override
Definition: attack.cpp:390
map_location target
Definition: contexts.hpp:74
double target_value
The value of the unit being targeted.
Definition: contexts.hpp:78
double avg_damage_inflicted
The average hitpoints damage inflicted.
Definition: contexts.hpp:87
double chance_to_kill
Estimated % chance to kill the unit.
Definition: contexts.hpp:84
bool attack_close(const map_location &loc) const
Definition: attack.cpp:257
wfl::variant execute_self(wfl::variant ctxt) override
Definition: attack.cpp:411
double terrain_quality
The weighted average of the % chance to hit each attacking unit.
Definition: contexts.hpp:98
double avg_damage_taken
The average hitpoints damage taken.
Definition: contexts.hpp:92
double alternative_terrain_quality
The weighted average of the % defense of the best possible terrain that the attacking units could rea...
Definition: contexts.hpp:105
bool leader_threat
Is true if the unit is a threat to our leader.
Definition: contexts.hpp:114
double avg_losses
The value on average, of units lost in the combat.
Definition: contexts.hpp:81
double vulnerability
The vulnerability is the power projection of enemy units onto the hex we're standing on.
Definition: contexts.hpp:111
double resources_used
The sum of the values of units used in the attack.
Definition: contexts.hpp:95
double rating(double aggression, const readonly_context &ai_obj) const
Definition: attack.cpp:270
bool is_surrounded
Is true if the units involved in this attack sequence are surrounded.
Definition: contexts.hpp:120
std::set< map_location > recent_attacks
Definition: game_info.hpp:115
static manager & get_singleton()
Definition: manager.hpp:145
game_info & get_ai_info()
Gets global AI-game info.
Definition: manager.cpp:712
virtual const defensive_position & best_defensive_position(const map_location &unit, const move_map &dstsrc, const move_map &srcdst, const move_map &enemy_dstsrc) const =0
virtual unit_stats_cache_t & unit_stats_cache() const =0
virtual const team & current_team() const =0
virtual double get_caution() const =0
virtual double get_leader_aggression() const =0
virtual attack_result_ptr execute_attack_action(const map_location &attacker_loc, const map_location &defender_loc, int attacker_weapon)=0
virtual move_result_ptr execute_move_action(const map_location &from, const map_location &to, bool remove_movement=true, bool unreach_is_ok=false)=0
Computes the statistics of a battle between an attacker and a defender unit.
Definition: attack.hpp:168
virtual const unit_map & units() const override
Definition: game_board.hpp:113
Encapsulates the map of the game.
Definition: map.hpp:172
bool is_village(const map_location &loc) const
Definition: map.cpp:66
int gives_healing(const map_location &loc) const
Definition: map.cpp:68
bool is_enemy(int n) const
Definition: team.hpp:231
Container associating units to locations.
Definition: map.hpp:99
unit_iterator end()
Definition: map.hpp:429
unit_ptr extract(const map_location &loc)
Extracts a unit from the map.
Definition: map.cpp:258
std::size_t count(const map_location &loc) const
Definition: map.hpp:414
unit_iterator find(std::size_t id)
Definition: map.cpp:301
umap_retval_pair_t insert(unit_ptr p)
Inserts the unit pointed to by p into the map.
Definition: map.cpp:134
umap_retval_pair_t move(const map_location &src, const map_location &dst)
Moves a unit from location src to location dst.
Definition: map.cpp:93
This class represents a single unit of a specific type.
Definition: unit.hpp:134
formula_callable_ptr fake_ptr()
Definition: callable.hpp:42
formula_input_vector inputs() const
Definition: callable.hpp:63
static void add_input(formula_input_vector &inputs, const std::string &key, formula_access access_type=formula_access::read_only)
Definition: callable.hpp:136
const_formula_callable_ptr as_callable() const
Definition: variant.hpp:83
Composite AI contexts.
Default AI contexts.
Defines formula ai.
std::size_t i
Definition: function.cpp:968
@ STATE_POISONED
The unit is slowed - it moves slower and does less damage.
Definition: unit.hpp:862
int attacks_left() const
Gets the remaining number of attacks this unit can perform this turn.
Definition: unit.hpp:995
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.cpp:475
std::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.cpp:546
Standard logging facilities (interface).
A small explanation about what's going on here: Each action has access to two game_info objects First...
Definition: actions.cpp:61
ai_context & get_ai_context(wfl::const_formula_callable_ptr for_fai)
std::shared_ptr< attack_result > attack_result_ptr
Definition: game_info.hpp:82
std::multimap< map_location, map_location > move_map
The standard way in which a map of possible moves is recorded.
Definition: game_info.hpp:43
std::shared_ptr< move_result > move_result_ptr
Definition: game_info.hpp:85
int kill_xp(int level)
Definition: game_config.hpp:48
int combat_xp(int level)
Definition: game_config.hpp:53
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:414
game_board * gameboard
Definition: resources.cpp:21
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
Definition: contexts.hpp:44
std::vector< formula_input > formula_input_vector
std::shared_ptr< const formula_callable > const_formula_callable_ptr
std::shared_ptr< unit > unit_ptr
Definition: ptr.hpp:26
ai_target::type type
Definition: contexts.hpp:36
All combat-related info.
std::vector< double > hp_dist
Resulting probability distribution (might be not as large as max_hp)
double average_hp(unsigned int healing=0) const
What's the average hp (weighted average of hp_dist).
Encapsulates the map of the game.
Definition: location.hpp:38
static map_location::DIRECTION n