The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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"
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
36static 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
40namespace ai {
41
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
268double 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
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.
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
static manager & get_singleton()
Definition manager.hpp:140
game_info & get_ai_info()
Gets global AI-game info.
Definition manager.cpp:567
virtual const team & current_team() const =0
virtual unit_stats_cache_t & unit_stats_cache() const =0
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 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:183
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:270
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.
#define LOG_AI
Definition contexts.cpp:62
Default AI contexts.
@ 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)
int combat_xp(int level)
std::shared_ptr< unit > unit_ptr
Definition ptr.hpp:26
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
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
#define i