The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
aspect_attacks.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2009 - 2025
3 by Yurii Chernyi <terraninfo@terraninfo.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 * Stage: fallback to other AI
18 * @file
19 */
20
22
23#include "actions/attack.hpp"
24#include "ai/manager.hpp"
25#include "game_board.hpp"
26#include "log.hpp"
27#include "lua/wrapper_lauxlib.h"
28#include "map/map.hpp"
29#include "pathfind/pathfind.hpp"
30#include "resources.hpp"
32#include "team.hpp"
33#include "units/filter.hpp"
34#include "units/unit.hpp"
35
36namespace ai
37{
38namespace ai_default_rca
39{
41#define DBG_AI LOG_STREAM(debug, log_ai_testing_aspect_attacks)
42#define LOG_AI LOG_STREAM(info, log_ai_testing_aspect_attacks)
43#define ERR_AI LOG_STREAM(err, log_ai_testing_aspect_attacks)
44
47{
48}
49
50aspect_attacks::aspect_attacks(readonly_context& context, const config& cfg, const std::string& id)
51 : aspect_attacks_base(context, cfg, id)
52 , filter_own_()
53 , filter_enemy_()
54{
55 if(auto filter_own = cfg.optional_child("filter_own")) {
56 vconfig vcfg(*filter_own);
57 vcfg.make_safe();
58 filter_own_.reset(new unit_filter(vcfg));
59 }
60
61 if(auto filter_enemy = cfg.optional_child("filter_enemy")) {
62 vconfig vcfg(*filter_enemy);
63 vcfg.make_safe();
64 filter_enemy_.reset(new unit_filter(vcfg));
65 }
66}
67
69{
70 this->value_ = analyze_targets();
71 this->valid_ = true;
72}
73
74std::shared_ptr<attacks_vector> aspect_attacks_base::analyze_targets() const
75{
76 const move_map& srcdst = get_srcdst();
77 const move_map& dstsrc = get_dstsrc();
78 const move_map& enemy_srcdst = get_enemy_srcdst();
79 const move_map& enemy_dstsrc = get_enemy_dstsrc();
80
81 auto res = std::make_shared<attacks_vector>();
82 const unit_map& units_ = resources::gameboard->units();
83
84 std::vector<map_location> unit_locs;
85 for(const unit& u : units_) {
86 if(u.side() == get_side() && u.attacks_left() && !(u.can_recruit() && is_passive_leader(u.id()))) {
87 if(!is_allowed_attacker(u)) {
88 continue;
89 }
90
91 unit_locs.push_back(u.get_location());
92 }
93 }
94
95 std::array<bool, 6> used_locations;
96 used_locations.fill(false);
97
98 moves_map dummy_moves;
99 move_map fullmove_srcdst, fullmove_dstsrc;
100 calculate_possible_moves(dummy_moves, fullmove_srcdst, fullmove_dstsrc, false, true);
101
102 unit_stats_cache().clear();
103
104 for(const unit& u : units_) {
105 // Attack anyone who is on the enemy side,
106 // and who is not invisible or petrified.
107 if(current_team().is_enemy(u.side()) && !u.incapacitated() && !u.invisible(u.get_location())) {
108 if(!is_allowed_enemy(u)) {
109 continue;
110 }
111
112 const auto adjacent = get_adjacent_tiles(u.get_location());
113 attack_analysis analysis;
114 analysis.target = u.get_location();
115 analysis.vulnerability = 0.0;
116 analysis.support = 0.0;
117
118 do_attack_analysis(u.get_location(), srcdst, dstsrc, fullmove_srcdst, fullmove_dstsrc, enemy_srcdst,
119 enemy_dstsrc, adjacent, used_locations, unit_locs, *res, analysis, current_team());
120 }
121 }
122 return res;
123}
124
126 const move_map& srcdst,
127 const move_map& dstsrc,
128 const move_map& fullmove_srcdst,
129 const move_map& fullmove_dstsrc,
130 const move_map& enemy_srcdst,
131 const move_map& enemy_dstsrc,
132 const std::array<map_location, 6>& tiles,
133 std::array<bool, 6>& used_locations,
134 std::vector<map_location>& units,
135 std::vector<attack_analysis>& result,
136 attack_analysis& cur_analysis,
137 const team& current_team) const
138{
139 // This function is called fairly frequently, so interact with the user here.
140
142 const int max_attack_depth = 5;
143 if(cur_analysis.movements.size() >= std::size_t(max_attack_depth)) {
144 return;
145 }
146
147 const gamemap& map_ = resources::gameboard->map();
149 const std::vector<team>& teams_ = resources::gameboard->teams();
150
151 const std::size_t max_positions = 1000;
152 if(result.size() > max_positions && !cur_analysis.movements.empty()) {
153 LOG_AI << "cut analysis short with number of positions";
154 return;
155 }
156
157 for(std::size_t i = 0; i != units.size(); ++i) {
158 const map_location current_unit = units[i];
159
160 unit_map::iterator unit_itor = units_.find(current_unit);
161 assert(unit_itor != units_.end());
162
163 // See if the unit has the backstab ability.
164 // Units with backstab will want to try to have a
165 // friendly unit opposite the position they move to.
166 //
167 // See if the unit has the slow ability -- units with slow only attack first.
168 bool backstab = false, slow = false;
169 for(const attack_type& a : unit_itor->attacks()) {
170 // For speed, just assume these specials will be active if they are present.
171 if (utils::find_if(a.specials(), [](const ability_ptr& p_ab) { return p_ab->id() == "backstab"; })) {
172 backstab = true;
173 }
174
175 if (utils::find_if(a.specials(), [](const ability_ptr& p_ab) { return p_ab->tag() == "slow"; })) {
176 slow = true;
177 }
178 }
179
180 if(slow && cur_analysis.movements.empty() == false) {
181 continue;
182 }
183
184 // Check if the friendly unit is surrounded,
185 // A unit is surrounded if it is flanked by enemy units
186 // and at least one other enemy unit is nearby
187 // or if the unit is totally surrounded by enemies
188 // with max. one tile to escape.
189 bool is_surrounded = false;
190 bool is_flanked = false;
191 int enemy_units_around = 0;
192 int accessible_tiles = 0;
193 const auto adj = get_adjacent_tiles(current_unit);
194
195 for(std::size_t tile = 0; tile != 3; ++tile) {
196 const unit_map::const_iterator tmp_unit = units_.find(adj[tile]);
197 bool possible_flanked = false;
198
199 if(map_.on_board(adj[tile])) {
200 ++accessible_tiles;
201 if(tmp_unit != units_.end() && current_team.is_enemy(tmp_unit->side())) {
202 ++enemy_units_around;
203 possible_flanked = true;
204 }
205 }
206
207 const unit_map::const_iterator tmp_opposite_unit = units_.find(adj[tile + 3]);
208 if(map_.on_board(adj[tile + 3])) {
209 ++accessible_tiles;
210 if(tmp_opposite_unit != units_.end() && current_team.is_enemy(tmp_opposite_unit->side())) {
211 ++enemy_units_around;
212 if(possible_flanked) {
213 is_flanked = true;
214 }
215 }
216 }
217 }
218
219 if((is_flanked && enemy_units_around > 2) || enemy_units_around >= accessible_tiles - 1) {
220 is_surrounded = true;
221 }
222
223 double best_vulnerability = 0.0, best_support = 0.0;
224 int best_rating = 0;
225 int cur_position = -1;
226
227 // Iterate over positions adjacent to the unit, finding the best rated one.
228 for(unsigned j = 0; j < tiles.size(); ++j) {
229 // If in this planned attack, a unit is already in this location.
230 if(used_locations[j]) {
231 continue;
232 }
233
234 // See if the current unit can reach that position.
235 if(tiles[j] != current_unit) {
236 auto its = dstsrc.equal_range(tiles[j]);
237 while(its.first != its.second) {
238 if(its.first->second == current_unit) {
239 break;
240 }
241
242 ++its.first;
243 }
244
245 // If the unit can't move to this location.
246 if(its.first == its.second || units_.find(tiles[j]) != units_.end()) {
247 continue;
248 }
249 }
250
251 int best_leadership_bonus = under_leadership(*unit_itor, tiles[j]);
252 double leadership_bonus = static_cast<double>(best_leadership_bonus + 100) / 100.0;
253 if(leadership_bonus > 1.1) {
254 LOG_AI << unit_itor->name() << " is getting leadership " << leadership_bonus;
255 }
256
257 // Check to see whether this move would be a backstab.
258 int backstab_bonus = 1;
259 double surround_bonus = 1.0;
260
261 if(tiles[(j + 3) % 6] != current_unit) {
262 const unit_map::const_iterator itor = units_.find(tiles[(j + 3) % 6]);
263
264 // Note that we *could* also check if a unit plans to move there
265 // before we're at this stage, but we don't because, since the
266 // attack calculations don't actually take backstab into account (too complicated),
267 // this could actually make our analysis look *worse* instead of better.
268 // So we only check for 'concrete' backstab opportunities.
269 // That would also break backstab_check, since it assumes
270 // the defender is in place.
271 if(itor != units_.end() && backstab_check(tiles[j], loc, units_, teams_)) {
272 if(backstab) {
273 backstab_bonus = 2;
274 }
275
276 // No surround bonus if target is skirmisher
277 if(!itor->get_ability_bool("skirmisher")) {
278 surround_bonus = 1.2;
279 }
280 }
281 }
282
283 // See if this position is the best rated we've seen so far.
284 int rating = static_cast<int>(rate_terrain(*unit_itor, tiles[j]) * backstab_bonus * leadership_bonus);
285 if(cur_position >= 0 && rating < best_rating) {
286 continue;
287 }
288
289 // Find out how vulnerable we are to attack from enemy units in this hex.
290 // FIXME: suokko's r29531 multiplied this by a constant 1.5. ?
291 const double vulnerability = power_projection(tiles[j], enemy_dstsrc); //?
292
293 // Calculate how much support we have on this hex from allies.
294 const double support = power_projection(tiles[j], fullmove_dstsrc); //?
295
296 // If this is a position with equal defense to another position,
297 // but more vulnerability then we don't want to use it.
298 if(cur_position >= 0 && rating == best_rating
299 && vulnerability / surround_bonus - support * surround_bonus >= best_vulnerability - best_support) {
300 continue;
301 }
302
303 cur_position = j;
304 best_rating = rating;
305 best_vulnerability = vulnerability / surround_bonus;
306 best_support = support * surround_bonus;
307 }
308
309 if(cur_position != -1) {
310 units.erase(units.begin() + i);
311
312 cur_analysis.movements.emplace_back(current_unit, tiles[cur_position]);
313 cur_analysis.vulnerability += best_vulnerability;
314 cur_analysis.support += best_support;
315 cur_analysis.is_surrounded = is_surrounded;
316 cur_analysis.analyze(map_, units_, *this, dstsrc, srcdst, enemy_dstsrc, get_aggression());
317 result.push_back(cur_analysis);
318
319 used_locations[cur_position] = true;
320
321 do_attack_analysis(loc, srcdst, dstsrc, fullmove_srcdst, fullmove_dstsrc, enemy_srcdst, enemy_dstsrc, tiles,
322 used_locations, units, result, cur_analysis, current_team);
323
324 used_locations[cur_position] = false;
325
326 cur_analysis.vulnerability -= best_vulnerability;
327 cur_analysis.support -= best_support;
328 cur_analysis.movements.pop_back();
329
330 units.insert(units.begin() + i, current_unit);
331 }
332 }
333}
334
336{
337 const gamemap& map_ = resources::gameboard->map();
338 const int defense = u.defense_modifier(map_.get_terrain(loc));
339 int rating = 100 - defense;
340
341 const int healing_value = 10;
342 const int friendly_village_value = 5;
343 const int neutral_village_value = 10;
344 const int enemy_village_value = 15;
345
346 if(map_.gives_healing(loc) && u.get_ability_bool("regenerate", loc) == false) {
347 rating += healing_value;
348 }
349
350 if(map_.is_village(loc)) {
352
353 if(owner == u.side()) {
354 rating += friendly_village_value;
355 } else if(owner == 0) {
356 rating += neutral_village_value;
357 } else {
358 rating += enemy_village_value;
359 }
360 }
361
362 return rating;
363}
364
366{
368 if(filter_own_ && !filter_own_->empty()) {
369 cfg.add_child("filter_own", filter_own_->to_config());
370 }
371
372 if(filter_enemy_ && !filter_enemy_->empty()) {
373 cfg.add_child("filter_enemy", filter_enemy_->to_config());
374 }
375
376 return cfg;
377}
378
380{
381 if(u.side() != get_side()) {
382 return false;
383 }
384
385 if(filter_own_) {
386 return (*filter_own_)(u);
387 }
388
389 return true;
390}
391
393{
394 const team& my_team = resources::gameboard->get_team(get_side());
395 if(!my_team.is_enemy(u.side())) {
396 return false;
397 }
398
399 if(filter_enemy_) {
400 return (*filter_enemy_)(u);
401 }
402
403 return true;
404}
405
406} // namespace ai_default_rca
407
409 readonly_context& context, const config& cfg, const std::string& id, std::shared_ptr<lua_ai_context>& l_ctx)
410 : aspect_attacks_base(context, cfg, id)
411 , handler_()
412 , code_()
413 , params_(cfg.child_or_empty("args"))
414{
415 this->name_ = "lua_aspect";
416 if(cfg.has_attribute("code")) {
417 code_ = cfg["code"].str();
418 } else if(cfg.has_attribute("value")) {
419 code_ = "return " + cfg["value"].apply_visitor(lua_aspect_visitor());
420 } else {
421 // error
422 return;
423 }
424
425 handler_.reset(resources::lua_kernel->create_lua_ai_action_handler(code_.c_str(), *l_ctx));
426}
427
429{
431 const config empty_cfg;
432 handler_->handle(params_, empty_cfg, true, obj_);
433
434 aspect_attacks_lua_filter filt = *obj_->get();
435 aspect_attacks_base::recalculate();
436
437 if(filt.lua) {
438 if(filt.ref_own_ != -1) {
439 luaL_unref(filt.lua, LUA_REGISTRYINDEX, filt.ref_own_);
440 }
441 if(filt.ref_enemy_ != -1) {
442 luaL_unref(filt.lua, LUA_REGISTRYINDEX, filt.ref_enemy_);
443 }
444 }
445
446 obj_.reset();
447}
448
450{
452 cfg["code"] = code_;
453 if(!params_.empty()) {
454 cfg.add_child("args", params_);
455 }
456
457 return cfg;
458}
459
460static bool call_lua_filter_fcn(lua_State* L, const unit& u, int idx)
461{
462 lua_rawgeti(L, LUA_REGISTRYINDEX, idx);
464 luaW_pcall(L, 1, 1);
465 bool result = luaW_toboolean(L, -1);
466 lua_pop(L, 1);
467 return result;
468}
469
471{
472 const aspect_attacks_lua_filter& filt = *obj_->get();
473 if(filt.lua && filt.ref_own_ != -1) {
474 return call_lua_filter_fcn(filt.lua, u, filt.ref_own_);
475 } else if(filt.filter_own_) {
476 return (*filt.filter_own_)(u);
477 } else {
478 return true;
479 }
480}
481
483{
484 const aspect_attacks_lua_filter& filt = *obj_->get();
485 if(filt.lua && filt.ref_enemy_ != -1) {
486 return call_lua_filter_fcn(filt.lua, u, filt.ref_enemy_);
487 } else if(filt.filter_enemy_) {
488 return (*filt.filter_enemy_)(u);
489 } else {
490 return true;
491 }
492}
493
494} // end of namespace ai
bool backstab_check(const map_location &attacker_loc, const map_location &defender_loc, const unit_map &units, const std::vector< team > &teams)
Function to check if an attack will satisfy the requirements for backstab.
Definition attack.cpp:1566
int under_leadership(const unit &u, const map_location &loc)
Tests if the unit at loc is currently affected by leadership.
Definition attack.cpp:1504
Various functions that implement attacks and attack calculations.
map_location loc
Definition move.cpp:172
Managing the AIs lifecycle - headers TODO: Refactor history handling and internal commands.
Aspect: attacks.
std::shared_ptr< attacks_vector > analyze_targets() const
virtual bool is_allowed_enemy(const unit &u) const =0
void do_attack_analysis(const map_location &loc, const move_map &srcdst, const move_map &dstsrc, const move_map &fullmove_srcdst, const move_map &fullmove_dstsrc, const move_map &enemy_srcdst, const move_map &enemy_dstsrc, const std::array< map_location, 6 > &tiles, std::array< bool, 6 > &used_locations, std::vector< map_location > &units, std::vector< attack_analysis > &result, attack_analysis &cur_analysis, const team &current_team) const
aspect_attacks_base(readonly_context &context, const config &cfg, const std::string &id)
static int rate_terrain(const unit &u, const map_location &loc)
virtual bool is_allowed_attacker(const unit &u) const =0
std::shared_ptr< unit_filter > filter_own_
virtual bool is_allowed_attacker(const unit &u) const
virtual bool is_allowed_enemy(const unit &u) const
std::shared_ptr< unit_filter > filter_enemy_
aspect_attacks(readonly_context &context, const config &cfg, const std::string &id)
virtual config to_config() const
virtual bool is_allowed_enemy(const unit &u) const
std::shared_ptr< lua_object< aspect_attacks_lua_filter > > obj_
virtual bool is_allowed_attacker(const unit &u) const
virtual void recalculate() const
aspect_attacks_lua(readonly_context &context, const config &cfg, const std::string &id, std::shared_ptr< lua_ai_context > &l_ctx)
std::shared_ptr< lua_ai_action_handler > handler_
virtual config to_config() const
Definition aspect.cpp:105
std::string name_
Definition aspect.hpp:89
bool valid_
Definition aspect.hpp:81
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
map_location target
Definition contexts.hpp:70
double vulnerability
The vulnerability is the power projection of enemy units onto the hex we're standing on.
Definition contexts.hpp:107
bool is_surrounded
Is true if the units involved in this attack sequence are surrounded.
Definition contexts.hpp:116
static manager & get_singleton()
Definition manager.hpp:140
void raise_user_interact()
Notifies all observers of 'ai_user_interact' event.
Definition manager.cpp:424
virtual const move_map & get_dstsrc() const override
Definition contexts.hpp:588
virtual const team & current_team() const override
Definition contexts.hpp:447
virtual unit_stats_cache_t & unit_stats_cache() const override
Definition contexts.hpp:858
virtual const move_map & get_enemy_srcdst() const override
Definition contexts.hpp:603
virtual void calculate_possible_moves(std::map< map_location, pathfind::paths > &possible_moves, move_map &srcdst, move_map &dstsrc, bool enemy, bool assume_full_movement=false, const terrain_filter *remove_destinations=nullptr) const override
Definition contexts.hpp:494
virtual bool is_passive_leader(const std::string &id) const override
Definition contexts.hpp:758
virtual double power_projection(const map_location &loc, const move_map &dstsrc) const override
Function which finds how much 'power' a side can attack a certain location with.
Definition contexts.hpp:673
virtual const move_map & get_enemy_dstsrc() const override
Definition contexts.hpp:593
virtual const move_map & get_srcdst() const override
Definition contexts.hpp:708
virtual double get_aggression() const override
Definition contexts.hpp:543
virtual side_number get_side() const override
Get the side number.
Definition contexts.hpp:393
std::shared_ptr< attacks_vector > value_
Definition aspect.hpp:142
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
config & add_child(std::string_view key)
Definition config.cpp:436
optional_config_impl< config > optional_child(std::string_view key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition config.cpp:380
bool has_attribute(std::string_view key) const
Definition config.cpp:157
bool empty() const
Definition config.cpp:823
int village_owner(const map_location &loc) const
Given the location of a village, will return the 1-based number of the team that currently owns it,...
virtual const gamemap & map() const override
virtual const std::vector< team > & teams() const override
team & get_team(int i)
virtual const unit_map & units() const override
terrain_code get_terrain(const map_location &loc) const
Looks up terrain at a particular location.
Definition map.cpp:265
bool on_board(const map_location &loc) const
Tell if a location is on the map.
Definition map.cpp:347
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
This class stores all the data for a single 'side' (in game nomenclature).
Definition team.hpp:74
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_iterator find(std::size_t id)
Definition map.cpp:302
This class represents a single unit of a specific type.
Definition unit.hpp:39
A variable-expanding proxy for the config class.
Definition variable.hpp:45
const vconfig & make_safe() const
instruct the vconfig to make a private copy of its underlying data.
Definition variable.cpp:164
#define LOG_AI
Definition contexts.cpp:62
const config * cfg
bool get_ability_bool(const std::string &tag_name, const map_location &loc) const
Checks whether this unit currently possesses or is affected by a given ability.
int side() const
The side this unit belongs to.
Definition unit.hpp:249
std::size_t underlying_id() const
This unit's unique internal ID.
Definition unit.hpp:298
int defense_modifier(const t_translation::terrain_code &terrain, const map_location &loc) const
The unit's defense on a given terrain.
Definition unit.cpp:1777
std::string id
Text to match against addon_info.tags()
Definition manager.cpp:199
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).
bool luaW_toboolean(lua_State *L, int n)
bool luaW_pcall(lua_State *L, int nArgs, int nRets, bool allow_wml_error)
Calls a Lua function stored below its nArgs arguments at the top of the stack.
lua_unit * luaW_pushunit(lua_State *L, Args... args)
Definition lua_unit.hpp:116
static lg::log_domain log_ai_testing_aspect_attacks("ai/aspect/attacks")
A small explanation about what's going on here: Each action has access to two game_info objects First...
Definition actions.cpp:59
std::vector< attack_analysis > attacks_vector
Definition game_info.hpp:52
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
static bool call_lua_filter_fcn(lua_State *L, const unit &u, int idx)
std::map< map_location, pathfind::paths > moves_map
The standard way in which a map of possible movement routes to location is recorded.
Definition game_info.hpp:46
game_board * gameboard
Definition resources.cpp:20
game_lua_kernel * lua_kernel
Definition resources.cpp:25
auto * find_if(Container &container, const Predicate &predicate)
Convenience wrapper for using find_if on a container without needing to comare to end()
Definition general.hpp:152
This module contains various pathfinding functions and utilities.
std::shared_ptr< unit_ability_t > ability_ptr
Definition ptr.hpp:38
std::shared_ptr< unit_filter > filter_own_
std::shared_ptr< unit_filter > filter_enemy_
Encapsulates the map of the game.
Definition location.hpp:46
#define i