The Battle for Wesnoth  1.19.0-dev
attack.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2024
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"
25 
26 #include "actions/attack.hpp"
27 #include "attack_prediction.hpp"
28 #include "game_config.hpp"
29 #include "log.hpp"
30 #include "map/map.hpp"
31 #include "team.hpp"
32 #include "units/unit.hpp"
33 #include "formula/callable_objects.hpp" // for location_callable
34 #include "resources.hpp"
35 #include "game_board.hpp"
36 
37 static lg::log_domain log_ai("ai/attack");
38 #define LOG_AI LOG_STREAM(info, log_ai)
39 #define ERR_AI LOG_STREAM(err, log_ai)
40 
41 namespace ai {
42 
43 extern ai_context& get_ai_context(wfl::const_formula_callable_ptr for_fai);
44 
45 void attack_analysis::analyze(const gamemap& map, unit_map& units,
46  const readonly_context& ai_obj,
47  const move_map& dstsrc, const move_map& srcdst,
48  const move_map& enemy_dstsrc, double aggression)
49 {
50  const unit_map::const_iterator defend_it = units.find(target);
51  assert(defend_it != units.end());
52 
53  // See if the target is a threat to our leader or an ally's leader.
54  const auto adj = get_adjacent_tiles(target);
55  std::size_t tile;
56  for(tile = 0; tile < adj.size(); ++tile) {
57  const unit_map::const_iterator leader = units.find(adj[tile]);
58  if(leader != units.end() && leader->can_recruit() && !ai_obj.current_team().is_enemy(leader->side())) {
59  break;
60  }
61  }
62 
63  leader_threat = (tile != 6);
64  uses_leader = false;
65 
66  target_value = defend_it->cost();
67  target_value += (static_cast<double>(defend_it->experience())/
68  static_cast<double>(defend_it->max_experience()))*target_value;
69  target_starting_damage = defend_it->max_hitpoints() -
70  defend_it->hitpoints();
71 
72  // Calculate the 'alternative_terrain_quality' -- the best possible defensive values
73  // the attacking units could hope to achieve if they didn't attack and moved somewhere.
74  // This is used for comparative purposes, to see just how vulnerable the AI is
75  // making itself.
77  double cost_sum = 0.0;
78  for(std::size_t i = 0; i != movements.size(); ++i) {
79  const unit_map::const_iterator att = units.find(movements[i].first);
80  const double cost = att->cost();
81  cost_sum += cost;
82  alternative_terrain_quality += cost*ai_obj.best_defensive_position(movements[i].first,dstsrc,srcdst,enemy_dstsrc).chance_to_hit;
83  }
84  alternative_terrain_quality /= cost_sum*100;
85 
87  avg_damage_taken = 0.0;
88  resources_used = 0.0;
89  terrain_quality = 0.0;
90  avg_losses = 0.0;
91  chance_to_kill = 0.0;
92 
93  double def_avg_experience = 0.0;
94  double first_chance_kill = 0.0;
95 
96  double prob_dead_already = 0.0;
97  assert(!movements.empty());
98  std::vector<std::pair<map_location,map_location>>::const_iterator m;
99 
100  std::unique_ptr<battle_context> bc(nullptr);
101  std::unique_ptr<battle_context> old_bc(nullptr);
102 
103  const combatant *prev_def = nullptr;
104 
105  for (m = movements.begin(); m != movements.end(); ++m) {
106  // We fix up units map to reflect what this would look like.
107  unit_ptr up = units.extract(m->first);
108  up->set_location(m->second);
109  units.insert(up);
110  double m_aggression = aggression;
111 
112  if (up->can_recruit()) {
113  uses_leader = true;
114  // FIXME: suokko's r29531 omitted this line
115  leader_threat = false;
116  m_aggression = ai_obj.get_leader_aggression();
117  }
118 
119  bool from_cache = false;
120 
121  // Swap the two context pointers. old_bc should be null at this point, so bc is cleared
122  // and old_bc takes ownership of the context pointer. This allows prev_def to remain
123  // valid until it's reassigned.
124  old_bc.swap(bc);
125 
126  // This cache is only about 99% correct, but speeds up evaluation by about 1000 times.
127  // We recalculate when we actually attack.
128  const readonly_context::unit_stats_cache_t::key_type cache_key = std::pair(target, &up->type());
129  const readonly_context::unit_stats_cache_t::iterator usc = ai_obj.unit_stats_cache().find(cache_key);
130  // Just check this attack is valid for this attacking unit (may be modified)
131  if (usc != ai_obj.unit_stats_cache().end() &&
132  usc->second.first.attack_num <
133  static_cast<int>(up->attacks().size())) {
134 
135  from_cache = true;
136  bc.reset(new battle_context(usc->second.first, usc->second.second));
137  } else {
138  bc.reset(new battle_context(units, m->second, target, -1, -1, m_aggression, prev_def));
139  }
140  const combatant &att = bc->get_attacker_combatant(prev_def);
141  const combatant &def = bc->get_defender_combatant(prev_def);
142 
143  prev_def = &bc->get_defender_combatant(prev_def);
144 
145  // We no longer need the old context since prev_def has been reassigned.
146  old_bc.reset(nullptr);
147 
148  if ( !from_cache ) {
149  ai_obj.unit_stats_cache().emplace(cache_key, std::pair(
150  bc->get_attacker_stats(),
151  bc->get_defender_stats()
152  ));
153  }
154 
155  // Note we didn't fight at all if defender is already dead.
156  double prob_fought = (1.0 - prob_dead_already);
157 
158  double prob_killed = def.hp_dist[0] - prob_dead_already;
159  prob_dead_already = def.hp_dist[0];
160 
161  double prob_died = att.hp_dist[0];
162  double prob_survived = (1.0 - prob_died) * prob_fought;
163 
164  double cost = up->cost();
165  const bool on_village = map.is_village(m->second);
166  // Up to double the value of a unit based on experience
167  cost += (static_cast<double>(up->experience()) / up->max_experience())*cost;
168  resources_used += cost;
169  avg_losses += cost * prob_died;
170 
171  // add half of cost for poisoned unit so it might get chance to heal
172  avg_losses += cost * up->get_state(unit::STATE_POISONED) /2;
173 
174  if (!bc->get_defender_stats().is_poisoned) {
175  avg_damage_inflicted += game_config::poison_amount * 2 * bc->get_defender_combatant().poisoned * (1 - prob_killed);
176  }
177 
178  // Double reward to emphasize getting onto villages if they survive.
179  if (on_village) {
180  avg_damage_taken -= game_config::poison_amount*2 * prob_survived;
181  }
182 
183  terrain_quality += (static_cast<double>(bc->get_defender_stats().chance_to_hit)/100.0)*cost * (on_village ? 0.5 : 1.0);
184 
185  double advance_prob = 0.0;
186  // The reward for advancing a unit is to get a 'negative' loss of that unit
187  if (!up->advances_to().empty()) {
188  int xp_for_advance = up->experience_to_advance();
189 
190  // See bug #6272... in some cases, unit already has got enough xp to advance,
191  // but hasn't (bug elsewhere?). Can cause divide by zero.
192  if (xp_for_advance == 0)
193  xp_for_advance = 1;
194 
195  int fight_xp = game_config::combat_xp(defend_it->level());
196  int kill_xp = game_config::kill_xp(fight_xp);
197 
198  if (fight_xp >= xp_for_advance) {
199  advance_prob = prob_fought;
200  avg_losses -= up->cost() * prob_fought;
201  } else if (kill_xp >= xp_for_advance) {
202  advance_prob = prob_killed;
203  avg_losses -= up->cost() * prob_killed;
204  // The reward for getting a unit closer to advancement
205  // (if it didn't advance) is to get the proportion of
206  // remaining experience needed, and multiply it by
207  // a quarter of the unit cost.
208  // This will cause the AI to heavily favor
209  // getting xp for close-to-advance units.
210  avg_losses -= up->cost() * 0.25 *
211  fight_xp * (prob_fought - prob_killed)
212  / xp_for_advance;
213  } else {
214  avg_losses -= up->cost() * 0.25 *
215  (kill_xp * prob_killed + fight_xp * (prob_fought - prob_killed))
216  / xp_for_advance;
217  }
218 
219  // The reward for killing with a unit that plagues
220  // is to get a 'negative' loss of that unit.
221  if (bc->get_attacker_stats().plagues) {
222  avg_losses -= prob_killed * up->cost();
223  }
224  }
225 
226  // If we didn't advance, we took this damage.
227  avg_damage_taken += (up->hitpoints() - att.average_hp()) * (1.0 - advance_prob);
228 
229  int fight_xp = game_config::combat_xp(up->level());
230  int kill_xp = game_config::kill_xp(fight_xp);
231  def_avg_experience += fight_xp * (1.0 - att.hp_dist[0]) + kill_xp * att.hp_dist[0];
232  if (m == movements.begin()) {
233  first_chance_kill = def.hp_dist[0];
234  }
235  }
236 
237  if (!defend_it->advances_to().empty() &&
238  def_avg_experience >= defend_it->experience_to_advance()) {
239  // It's likely to advance: only if we can kill with first blow.
240  chance_to_kill = first_chance_kill;
241  // Negative average damage (it will advance).
242  avg_damage_inflicted += defend_it->hitpoints() - defend_it->max_hitpoints();
243  } else {
244  chance_to_kill = prev_def->hp_dist[0];
245  avg_damage_inflicted += defend_it->hitpoints() - prev_def->average_hp(map.gives_healing(defend_it->get_location()));
246  }
247 
249 
250  // Restore the units to their original positions.
251  for (m = movements.begin(); m != movements.end(); ++m) {
252  units.move(m->second, m->first);
253  }
254 }
255 
257 {
258  std::set<map_location> &attacks = manager::get_singleton().get_ai_info().recent_attacks;
259  for(std::set<map_location>::const_iterator i = attacks.begin(); i != attacks.end(); ++i) {
260  if(distance_between(*i,loc) < 4) {
261  return true;
262  }
263  }
264 
265  return false;
266 }
267 
268 
269 double attack_analysis::rating(double aggression, const readonly_context& ai_obj) const
270 {
271  if(leader_threat) {
272  aggression = 1.0;
273  }
274 
275  if(uses_leader) {
276  aggression = ai_obj.get_leader_aggression();
277  }
278 
279  double value = chance_to_kill*target_value - avg_losses*(1.0-aggression);
280 
282  // This situation looks like it might be a bad move:
283  // we are moving our attackers out of their optimal terrain
284  // into sub-optimal terrain.
285  // Calculate the 'exposure' of our units to risk.
286 
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  LOG_AI << "attack option has base value " << value << " with exposure " << exposure << ": "
290  << vulnerability << "/" << support << " = " << (vulnerability/std::max<double>(support,0.1));
291  value -= exposure*(1.0-aggression);
292  }
293 
294  // Prefer to attack already damaged targets.
295  value += ((target_starting_damage/3 + avg_damage_inflicted) - (1.0-aggression)*avg_damage_taken)/10.0;
296 
297  // If the unit is surrounded and there is no support,
298  // or if the unit is surrounded and the average damage is 0,
299  // the unit skips its sanity check and tries to break free as good as possible.
300  if(!is_surrounded || (support != 0 && avg_damage_taken != 0))
301  {
302  // Sanity check: if we're putting ourselves at major risk,
303  // and have no chance to kill, and we're not aiding our allies
304  // who are also attacking, then don't do it.
305  if(vulnerability > 50.0 && vulnerability > support*2.0
306  && chance_to_kill < 0.02 && aggression < 0.75
307  && !attack_close(target)) {
308  return -1.0;
309  }
310  }
311 
312  if(!leader_threat && vulnerability*terrain_quality > 0.0 && support != 0) {
314  }
315 
316  value /= ((resources_used/2) + (resources_used/2)*terrain_quality);
317 
318  if(leader_threat) {
319  value *= 5.0;
320  }
321 
322  LOG_AI << "attack on " << target << ": attackers: " << movements.size()
323  << " value: " << value << " chance to kill: " << chance_to_kill
324  << " damage inflicted: " << avg_damage_inflicted
325  << " damage taken: " << avg_damage_taken
326  << " vulnerability: " << vulnerability
327  << " support: " << support
328  << " quality: " << terrain_quality
329  << " alternative quality: " << alternative_terrain_quality;
330 
331  return value;
332 }
333 
334 wfl::variant attack_analysis::get_value(const std::string& key) const
335 {
336  using namespace wfl;
337  if(key == "target") {
338  return variant(std::make_shared<location_callable>(target));
339  } else if(key == "movements") {
340  std::vector<variant> res;
341  for(std::size_t n = 0; n != movements.size(); ++n) {
342  auto item = std::make_shared<map_formula_callable>(nullptr);
343  item->add("src", variant(std::make_shared<location_callable>(movements[n].first)));
344  item->add("dst", variant(std::make_shared<location_callable>(movements[n].second)));
345  res.emplace_back(item);
346  }
347 
348  return variant(res);
349  } else if(key == "units") {
350  std::vector<variant> res;
351  for(std::size_t n = 0; n != movements.size(); ++n) {
352  res.emplace_back(std::make_shared<location_callable>(movements[n].first));
353  }
354 
355  return variant(res);
356  } else if(key == "target_value") {
357  return variant(static_cast<int>(target_value*1000));
358  } else if(key == "avg_losses") {
359  return variant(static_cast<int>(avg_losses*1000));
360  } else if(key == "chance_to_kill") {
361  return variant(static_cast<int>(chance_to_kill*100));
362  } else if(key == "avg_damage_inflicted") {
363  return variant(static_cast<int>(avg_damage_inflicted));
364  } else if(key == "target_starting_damage") {
366  } else if(key == "avg_damage_taken") {
367  return variant(static_cast<int>(avg_damage_taken));
368  } else if(key == "resources_used") {
369  return variant(static_cast<int>(resources_used));
370  } else if(key == "terrain_quality") {
371  return variant(static_cast<int>(terrain_quality));
372  } else if(key == "alternative_terrain_quality") {
373  return variant(static_cast<int>(alternative_terrain_quality));
374  } else if(key == "vulnerability") {
375  return variant(static_cast<int>(vulnerability));
376  } else if(key == "support") {
377  return variant(static_cast<int>(support));
378  } else if(key == "leader_threat") {
379  return variant(leader_threat);
380  } else if(key == "uses_leader") {
381  return variant(uses_leader);
382  } else if(key == "is_surrounded") {
383  return variant(is_surrounded);
384  } else {
385  return variant();
386  }
387 }
388 
390 {
391  add_input(inputs, "target");
392  add_input(inputs, "movements");
393  add_input(inputs, "units");
394  add_input(inputs, "target_value");
395  add_input(inputs, "avg_losses");
396  add_input(inputs, "chance_to_kill");
397  add_input(inputs, "avg_damage_inflicted");
398  add_input(inputs, "target_starting_damage");
399  add_input(inputs, "avg_damage_taken");
400  add_input(inputs, "resources_used");
401  add_input(inputs, "terrain_quality");
402  add_input(inputs, "alternative_terrain_quality");
403  add_input(inputs, "vulnerability");
404  add_input(inputs, "support");
405  add_input(inputs, "leader_threat");
406  add_input(inputs, "uses_leader");
407  add_input(inputs, "is_surrounded");
408 }
409 
411  //If we get an attack analysis back we will do the first attack.
412  //Then the AI can get run again and re-choose.
413  if(movements.empty()) {
414  return wfl::variant(false);
415  }
416 
417  unit_map& units = resources::gameboard->units();
418 
419  //make sure that unit which has to attack is at given position and is able to attack
420  unit_map::const_iterator unit = units.find(movements.front().first);
421  if(!unit.valid() || unit->attacks_left() == 0) {
422  return wfl::variant(false);
423  }
424 
425  const map_location& move_from = movements.front().first;
426  const map_location& att_src = movements.front().second;
427  const map_location& att_dst = target;
428 
429  //check if target is still valid
430  unit = units.find(att_dst);
431  if(unit == units.end()) {
432  return wfl::variant(std::make_shared<wfl::safe_call_result>(fake_ptr(), attack_result::E_EMPTY_DEFENDER, move_from));
433  }
434 
435  //check if we need to move
436  if(move_from != att_src) {
437  //now check if location to which we want to move is still unoccupied
438  unit = units.find(att_src);
439  if(unit != units.end()) {
440  return wfl::variant(std::make_shared<wfl::safe_call_result>(fake_ptr(), move_result::E_NO_UNIT, move_from));
441  }
442 
443  ai::move_result_ptr result = get_ai_context(ctxt.as_callable()).execute_move_action(move_from, att_src);
444  if(!result->is_ok()) {
445  //move part failed
446  LOG_AI << "ERROR #" << result->get_status() << " while executing 'attack' formula function";
447  return wfl::variant(std::make_shared<wfl::safe_call_result>(fake_ptr(), result->get_status(), result->get_unit_location()));
448  }
449  }
450 
451  if(units.count(att_src)) {
453  if(!result->is_ok()) {
454  //attack failed
455  LOG_AI << "ERROR #" << result->get_status() << " while executing 'attack' formula function";
456  return wfl::variant(std::make_shared<wfl::safe_call_result>(fake_ptr(), result->get_status()));
457  }
458  }
459  return wfl::variant(true);
460 }
461 
462 } //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:38
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:45
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:334
void get_inputs(wfl::formula_input_vector &inputs) const override
Definition: attack.cpp:389
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:256
wfl::variant execute_self(wfl::variant ctxt) override
Definition: attack.cpp:410
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:269
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:142
game_info & get_ai_info()
Gets global AI-game info.
Definition: manager.cpp:710
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:167
virtual const unit_map & units() const override
Definition: game_board.hpp:106
Encapsulates the map of the game.
Definition: map.hpp:172
bool is_village(const map_location &loc) const
Definition: map.cpp:65
int gives_healing(const map_location &loc) const
Definition: map.cpp:67
bool is_enemy(int n) const
Definition: team.hpp:229
Container associating units to locations.
Definition: map.hpp:98
unit_iterator end()
Definition: map.hpp:428
unit_ptr extract(const map_location &loc)
Extracts a unit from the map.
Definition: map.cpp:259
std::size_t count(const map_location &loc) const
Definition: map.hpp:413
unit_iterator find(std::size_t id)
Definition: map.cpp:302
umap_retval_pair_t insert(unit_ptr p)
Inserts the unit pointed to by p into the map.
Definition: map.cpp:135
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:92
This class represents a single unit of a specific type.
Definition: unit.hpp:133
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.
std::size_t i
Definition: function.cpp:968
@ STATE_POISONED
The unit is slowed - it moves slower and does less damage.
Definition: unit.hpp:861
int attacks_left() const
Gets the remaining number of attacks this unit can perform this turn.
Definition: unit.hpp:994
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:474
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:545
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:59
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:47
int combat_xp(int level)
Definition: game_config.hpp:52
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:412
game_board * gameboard
Definition: resources.cpp:20
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
Definition: contexts.hpp:43
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