The Battle for Wesnoth  1.19.22+dev
attack.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2003 - 2025
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 "resources.hpp"
34 #include "game_board.hpp"
35 
36 static lg::log_domain log_ai("ai/attack");
37 #define LOG_AI LOG_STREAM(info, log_ai)
38 #define ERR_AI LOG_STREAM(err, log_ai)
39 
40 namespace ai {
41 
42 void attack_analysis::analyze(const gamemap& map, unit_map& units,
43  const readonly_context& ai_obj,
44  const move_map& dstsrc, const move_map& srcdst,
45  const move_map& enemy_dstsrc, double aggression)
46 {
47  const unit_map::const_iterator defend_it = units.find(target);
48  assert(defend_it != units.end());
49 
50  // See if the target is a threat to our leader or an ally's leader.
51  const auto adj = get_adjacent_tiles(target);
52  std::size_t tile;
53  for(tile = 0; tile < adj.size(); ++tile) {
54  const unit_map::const_iterator leader = units.find(adj[tile]);
55  if(leader != units.end() && leader->can_recruit() && !ai_obj.current_team().is_enemy(leader->side())) {
56  break;
57  }
58  }
59 
60  leader_threat = (tile != 6);
61  uses_leader = false;
62 
63  target_value = defend_it->cost();
64  const unsigned int defend_experience = defend_it->can_advance() ? defend_it->experience() : 0;
65  target_value += (static_cast<double>(defend_experience)/
66  static_cast<double>(defend_it->max_experience()))*target_value;
67  target_starting_damage = defend_it->max_hitpoints() -
68  defend_it->hitpoints();
69 
70  // Calculate the 'alternative_terrain_quality' -- the best possible defensive values
71  // the attacking units could hope to achieve if they didn't attack and moved somewhere.
72  // This is used for comparative purposes, to see just how vulnerable the AI is
73  // making itself.
75  double cost_sum = 0.0;
76  for(std::size_t i = 0; i != movements.size(); ++i) {
77  const unit_map::const_iterator att = units.find(movements[i].first);
78  const double cost = att->cost();
79  cost_sum += cost;
80  alternative_terrain_quality += cost*ai_obj.best_defensive_position(movements[i].first,dstsrc,srcdst,enemy_dstsrc).chance_to_hit;
81  }
82  alternative_terrain_quality /= cost_sum*100;
83 
85  avg_damage_taken = 0.0;
86  resources_used = 0.0;
87  terrain_quality = 0.0;
88  avg_losses = 0.0;
89  chance_to_kill = 0.0;
90 
91  double def_avg_experience = 0.0;
92  double first_chance_kill = 0.0;
93 
94  double prob_dead_already = 0.0;
95  assert(!movements.empty());
96  std::vector<std::pair<map_location,map_location>>::const_iterator m;
97 
98  std::unique_ptr<battle_context> bc(nullptr);
99  std::unique_ptr<battle_context> old_bc(nullptr);
100 
101  const combatant *prev_def = nullptr;
102 
103  for (m = movements.begin(); m != movements.end(); ++m) {
104  // We fix up units map to reflect what this would look like.
105  unit_ptr up = units.extract(m->first);
106  up->set_location(m->second);
107  units.insert(up);
108  double m_aggression = aggression;
109 
110  if (up->can_recruit()) {
111  uses_leader = true;
112  // FIXME: suokko's r29531 omitted this line
113  leader_threat = false;
114  m_aggression = ai_obj.get_leader_aggression();
115  }
116 
117  bool from_cache = false;
118 
119  // Swap the two context pointers. old_bc should be null at this point, so bc is cleared
120  // and old_bc takes ownership of the context pointer. This allows prev_def to remain
121  // valid until it's reassigned.
122  old_bc.swap(bc);
123 
124  // This cache is only about 99% correct, but speeds up evaluation by about 1000 times.
125  // We recalculate when we actually attack.
126  const readonly_context::unit_stats_cache_t::key_type cache_key = std::pair(target, &up->type());
127  const readonly_context::unit_stats_cache_t::iterator usc = ai_obj.unit_stats_cache().find(cache_key);
128  // Just check this attack is valid for this attacking unit (may be modified)
129  if (usc != ai_obj.unit_stats_cache().end() &&
130  usc->second.first.attack_num <
131  static_cast<int>(up->attacks().size())) {
132 
133  from_cache = true;
134  bc.reset(new battle_context(usc->second.first, usc->second.second));
135  } else {
136  bc.reset(new battle_context(units, m->second, target, -1, -1, m_aggression, prev_def));
137  }
138  const combatant &att = bc->get_attacker_combatant(prev_def);
139  const combatant &def = bc->get_defender_combatant(prev_def);
140 
141  prev_def = &bc->get_defender_combatant(prev_def);
142 
143  // We no longer need the old context since prev_def has been reassigned.
144  old_bc.reset(nullptr);
145 
146  if ( !from_cache ) {
147  ai_obj.unit_stats_cache().emplace(cache_key, std::pair(
148  bc->get_attacker_stats(),
149  bc->get_defender_stats()
150  ));
151  }
152 
153  // Note we didn't fight at all if defender is already dead.
154  double prob_fought = (1.0 - prob_dead_already);
155 
156  double prob_killed = def.hp_dist[0] - prob_dead_already;
157  prob_dead_already = def.hp_dist[0];
158 
159  double prob_died = att.hp_dist[0];
160  double prob_survived = (1.0 - prob_died) * prob_fought;
161 
162  double cost = up->cost();
163  const bool on_village = map.is_village(m->second);
164  // Up to double the value of a unit based on experience
165  const unsigned int up_experience = up->can_advance() ? up->experience() : 0;
166  cost += (static_cast<double>(up_experience) / up->max_experience())*cost;
167  resources_used += cost;
168  avg_losses += cost * prob_died;
169 
170  // add half of cost for poisoned unit so it might get chance to heal
171  avg_losses += cost * up->get_state(unit::STATE_POISONED) /2;
172 
173  if (!bc->get_defender_stats().is_poisoned) {
174  avg_damage_inflicted += game_config::poison_amount * 2 * bc->get_defender_combatant().poisoned * (1 - prob_killed);
175  }
176 
177  // Double reward to emphasize getting onto villages if they survive.
178  if (on_village) {
179  avg_damage_taken -= game_config::poison_amount*2 * prob_survived;
180  }
181 
182  terrain_quality += (static_cast<double>(bc->get_defender_stats().chance_to_hit)/100.0)*cost * (on_village ? 0.5 : 1.0);
183 
184  double advance_prob = 0.0;
185  // The reward for advancing a unit is to get a 'negative' loss of that unit
186  if (!up->advances_to().empty()) {
187  int xp_for_advance = up->experience_to_advance();
188 
189  // See bug #6272... in some cases, unit already has got enough xp to advance,
190  // but hasn't (bug elsewhere?). Can cause divide by zero.
191  if (xp_for_advance == 0)
192  xp_for_advance = 1;
193 
194  int fight_xp = game_config::combat_xp(defend_it->level());
195  int kill_xp = game_config::kill_xp(fight_xp);
196 
197  if (fight_xp >= xp_for_advance) {
198  advance_prob = prob_fought;
199  avg_losses -= up->cost() * prob_fought;
200  } else if (kill_xp >= xp_for_advance) {
201  advance_prob = prob_killed;
202  avg_losses -= up->cost() * prob_killed;
203  // The reward for getting a unit closer to advancement
204  // (if it didn't advance) is to get the proportion of
205  // remaining experience needed, and multiply it by
206  // a quarter of the unit cost.
207  // This will cause the AI to heavily favor
208  // getting xp for close-to-advance units.
209  avg_losses -= up->cost() * 0.25 *
210  fight_xp * (prob_fought - prob_killed)
211  / xp_for_advance;
212  } else {
213  avg_losses -= up->cost() * 0.25 *
214  (kill_xp * prob_killed + fight_xp * (prob_fought - prob_killed))
215  / xp_for_advance;
216  }
217 
218  // The reward for killing with a unit that plagues
219  // is to get a 'negative' loss of that unit.
220  if (bc->get_attacker_stats().plagues) {
221  avg_losses -= prob_killed * up->cost();
222  }
223  }
224 
225  // If we didn't advance, we took this damage.
226  avg_damage_taken += (up->hitpoints() - att.average_hp()) * (1.0 - advance_prob);
227 
228  int fight_xp = game_config::combat_xp(up->level());
229  int kill_xp = game_config::kill_xp(fight_xp);
230  def_avg_experience += fight_xp * (1.0 - att.hp_dist[0]) + kill_xp * att.hp_dist[0];
231  if (m == movements.begin()) {
232  first_chance_kill = def.hp_dist[0];
233  }
234  }
235 
236  if (!defend_it->advances_to().empty() &&
237  def_avg_experience >= defend_it->experience_to_advance()) {
238  // It's likely to advance: only if we can kill with first blow.
239  chance_to_kill = first_chance_kill;
240  // Negative average damage (it will advance).
241  avg_damage_inflicted += defend_it->hitpoints() - defend_it->max_hitpoints();
242  } else {
243  chance_to_kill = prev_def->hp_dist[0];
244  avg_damage_inflicted += defend_it->hitpoints() - prev_def->average_hp(map.gives_healing(defend_it->get_location()));
245  }
246 
248 
249  // Restore the units to their original positions.
250  for (m = movements.begin(); m != movements.end(); ++m) {
251  units.move(m->second, m->first);
252  }
253 }
254 
256 {
257  std::set<map_location> &attacks = manager::get_singleton().get_ai_info().recent_attacks;
258  for(std::set<map_location>::const_iterator i = attacks.begin(); i != attacks.end(); ++i) {
259  if(distance_between(*i,loc) < 4) {
260  return true;
261  }
262  }
263 
264  return false;
265 }
266 
267 
268 double attack_analysis::rating(double aggression, const readonly_context& ai_obj) const
269 {
270  if(leader_threat) {
271  aggression = 1.0;
272  }
273 
274  if(uses_leader) {
275  aggression = ai_obj.get_leader_aggression();
276  }
277 
278  double value = chance_to_kill*target_value - avg_losses*(1.0-aggression);
279 
281  // This situation looks like it might be a bad move:
282  // we are moving our attackers out of their optimal terrain
283  // into sub-optimal terrain.
284  // Calculate the 'exposure' of our units to risk.
285 
286  const double exposure_mod = uses_leader ? 2.0 : ai_obj.get_caution();
287  const double exposure = exposure_mod*resources_used*(terrain_quality - alternative_terrain_quality)*vulnerability/std::max<double>(0.01,support);
288  LOG_AI << "attack option has base value " << value << " with exposure " << exposure << ": "
289  << vulnerability << "/" << support << " = " << (vulnerability/std::max<double>(support,0.1));
290  value -= exposure*(1.0-aggression);
291  }
292 
293  // Prefer to attack already damaged targets.
294  value += ((target_starting_damage/3 + avg_damage_inflicted) - (1.0-aggression)*avg_damage_taken)/10.0;
295 
296  // If the unit is surrounded and there is no support,
297  // or if the unit is surrounded and the average damage is 0,
298  // the unit skips its sanity check and tries to break free as good as possible.
299  if(!is_surrounded || (support != 0 && avg_damage_taken != 0))
300  {
301  // Sanity check: if we're putting ourselves at major risk,
302  // and have no chance to kill, and we're not aiding our allies
303  // who are also attacking, then don't do it.
304  if(vulnerability > 50.0 && vulnerability > support*2.0
305  && chance_to_kill < 0.02 && aggression < 0.75
306  && !attack_close(target)) {
307  return -1.0;
308  }
309  }
310 
311  if(!leader_threat && vulnerability*terrain_quality > 0.0 && support != 0) {
313  }
314 
315  value /= ((resources_used/2) + (resources_used/2)*terrain_quality);
316 
317  if(leader_threat) {
318  value *= 5.0;
319  }
320 
321  LOG_AI << "attack on " << target << ": attackers: " << movements.size()
322  << " value: " << value << " chance to kill: " << chance_to_kill
323  << " damage inflicted: " << avg_damage_inflicted
324  << " damage taken: " << avg_damage_taken
325  << " vulnerability: " << vulnerability
326  << " support: " << support
327  << " quality: " << terrain_quality
328  << " alternative quality: " << alternative_terrain_quality;
329 
330  return value;
331 }
332 
333 } //end of namespace ai
Various functions that implement attacks and attack calculations.
map_location loc
Definition: move.cpp:172
Managing the AI-Game interaction - AI actions and their results.
#define LOG_AI
Definition: attack.cpp:37
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:71
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:42
bool uses_leader
Is true if this attack sequence makes use of the leader.
Definition: contexts.hpp:113
double target_value
The value of the unit being targeted.
Definition: contexts.hpp:74
double avg_damage_inflicted
The average hitpoints damage inflicted.
Definition: contexts.hpp:83
double chance_to_kill
Estimated % chance to kill the unit.
Definition: contexts.hpp:80
bool attack_close(const map_location &loc) const
Definition: attack.cpp:255
double terrain_quality
The weighted average of the % chance to hit each attacking unit.
Definition: contexts.hpp:94
double avg_damage_taken
The average hitpoints damage taken.
Definition: contexts.hpp:88
double alternative_terrain_quality
The weighted average of the % defense of the best possible terrain that the attacking units could rea...
Definition: contexts.hpp:101
bool leader_threat
Is true if the unit is a threat to our leader.
Definition: contexts.hpp:110
double avg_losses
The value on average, of units lost in the combat.
Definition: contexts.hpp:77
double vulnerability
The vulnerability is the power projection of enemy units onto the hex we're standing on.
Definition: contexts.hpp:107
double resources_used
The sum of the values of units used in the attack.
Definition: contexts.hpp:91
double rating(double aggression, const readonly_context &ai_obj) const
Definition: attack.cpp:268
bool is_surrounded
Is true if the units involved in this attack sequence are surrounded.
Definition: contexts.hpp:116
std::set< map_location > recent_attacks
Definition: game_info.hpp:115
static manager & get_singleton()
Definition: manager.hpp:140
game_info & get_ai_info()
Gets global AI-game info.
Definition: manager.cpp:567
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
Computes the statistics of a battle between an attacker and a defender unit.
Definition: attack.hpp:165
Encapsulates the map of the game.
Definition: map.hpp:176
bool is_village(const map_location &loc) const
Definition: map.cpp:60
int gives_healing(const map_location &loc) const
Definition: map.cpp:62
bool is_enemy(int n) const
Definition: team.hpp:267
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
unit_iterator find(std::size_t id)
Definition: map.cpp:302
umap_retval_pair_t insert(const 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
Composite AI contexts.
Default AI contexts.
std::size_t i
Definition: function.cpp:1031
@ STATE_POISONED
The unit is slowed - it moves slower and does less damage.
Definition: unit.hpp:776
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:584
void get_adjacent_tiles(const map_location &a, utils::span< map_location, 6 > res)
Function which, given a location, will place all adjacent locations in res.
Definition: location.cpp:513
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
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
int kill_xp(int level)
Definition: game_config.hpp:48
int combat_xp(int level)
Definition: game_config.hpp:53
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
std::shared_ptr< unit > unit_ptr
Definition: ptr.hpp:26
ai_target::type type
Definition: contexts.hpp:35
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:46