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