The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
statistics_record.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2023 - 2025
3 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
4
5 This program is free software; you can redistribute it and/or modify
6 it under the terms of the GNU General Public License as published by
7 the Free Software Foundation; either version 2 of the License, or
8 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY.
11
12 See the COPYING file for more details.
13*/
14
15/**
16 * @file
17 * Manage statistics: saving and reading data.
18 */
19
20#include "statistics_record.hpp"
21#include "log.hpp"
24
25
26static lg::log_domain log_engine("engine");
27#define DBG_NG LOG_STREAM(debug, log_engine)
28#define ERR_NG LOG_STREAM(err, log_engine)
29
31{
32
34{
35 config res;
36 for(stats_t::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
37 std::string n = std::to_string(i->second);
38 if(res.has_attribute(n)) {
39 res[n] = res[n].str() + "," + i->first;
40 } else {
41 res[n] = i->first;
42 }
43 }
44
45 return res;
46}
47
49{
50 using reverse_map = std::multimap<int, std::string>;
51 reverse_map rev;
52 std::transform(m.begin(), m.end(), std::inserter(rev, rev.begin()),
53 [](const stats_t::str_int_map::value_type& p) { return std::pair(p.second, p.first); });
54 reverse_map::const_iterator i = rev.begin(), j;
55 while(i != rev.end()) {
56 j = rev.upper_bound(i->first);
57 std::vector<std::string> vals;
58 std::transform(i, j, std::back_inserter(vals), [](const reverse_map::value_type& p) { return p.second; });
59 out.write_key_val(std::to_string(i->first), utils::join(vals));
60 i = j;
61 }
62}
63
65{
67 for(const auto& [key, value] : cfg.attribute_range()) {
68 try {
69 for(const std::string& val : utils::split(value)) {
70 m[val] = std::stoi(key);
71 }
72 } catch(const std::invalid_argument&) {
73 ERR_NG << "Invalid statistics entry; skipping";
74 }
75 }
76
77 return m;
78}
79
81{
82 config res;
83 for(stats_t::battle_result_map::const_iterator i = m.begin(); i != m.end(); ++i) {
84 config& new_cfg = res.add_child("sequence");
85 new_cfg = write_str_int_map(i->second);
86 new_cfg["_num"] = i->first;
87 }
88
89 return res;
90}
91
93{
94 for(stats_t::battle_result_map::const_iterator i = m.begin(); i != m.end(); ++i) {
95 out.open_child("sequence");
96 write_str_int_map(out, i->second);
97 out.write_key_val("_num", i->first);
98 out.close_child("sequence");
99 }
100}
101
103{
105 for(const config& i : cfg.child_range("sequence")) {
106 config item = i;
107 int key = item["_num"].to_int();
108 item.remove_attribute("_num");
109 m[key] = read_str_int_map(item);
110 }
111
112 return m;
113}
114
116{
117 config res;
118 for(const auto& i : m) {
119 res.add_child("hitrate_map_entry", config{"cth", i.first, "stats", i.second.write()});
120 }
121 return res;
122}
123
125
127 const stats_t::battle_result_map& attacks, const stats_t::battle_result_map& defends)
128{
130
131 stats_t::battle_result_map merged = attacks;
132 merge_battle_result_maps(merged, defends);
133
134 for(const auto& i : merged) {
135 int cth = i.first;
136 const stats_t::battle_sequence_frequency_map& frequency_map = i.second;
137 for(const auto& j : frequency_map) {
138 const std::string& res = j.first; // see attack_context::~attack_context()
139 const int occurrences = j.second;
140 unsigned int misses = std::count(res.begin(), res.end(), '0');
141 unsigned int hits = std::count(res.begin(), res.end(), '1');
142 if(misses + hits == 0) {
143 continue;
144 }
145 misses *= occurrences;
146 hits *= occurrences;
147 m[cth].strikes += misses + hits;
148 m[cth].hits += hits;
149 }
150 }
151
152 return m;
153}
154
156{
158 for(const config& i : cfg.child_range("hitrate_map_entry")) {
159 m.emplace(i["cth"].to_int(), stats_t::hitrate_t(i.mandatory_child("stats")));
160 }
161 return m;
162}
163
165{
166 for(stats_t::str_int_map::const_iterator i = b.begin(); i != b.end(); ++i) {
167 a[i->first] += i->second;
168 }
169}
170
172{
173 for(stats_t::battle_result_map::const_iterator i = b.begin(); i != b.end(); ++i) {
174 merge_str_int_map(a[i->first], i->second);
175 }
176}
177
179{
180 for(const auto& i : b) {
181 a[i.first].hits += i.second.hits;
182 a[i.first].strikes += i.second.strikes;
183 }
184}
185
186
188 : recruits()
189 , recalls()
190 , advanced_to()
191 , deaths()
192 , killed()
193 , recruit_cost(0)
194 , recall_cost(0)
195 , attacks_inflicted()
196 , defends_inflicted()
197 , attacks_taken()
198 , defends_taken()
199 , damage_inflicted(0)
200 , damage_taken(0)
201 , turn_damage_inflicted(0)
202 , turn_damage_taken(0)
203 , by_cth_inflicted()
204 , by_cth_taken()
205 , turn_by_cth_inflicted()
206 , turn_by_cth_taken()
207 , expected_damage_inflicted(0)
208 , expected_damage_taken(0)
209 , turn_expected_damage_inflicted(0)
210 , turn_expected_damage_taken(0)
211 , save_id()
212{
213}
214
216 : recruits()
217 , recalls()
218 , advanced_to()
219 , deaths()
220 , killed()
221 , recruit_cost(0)
222 , recall_cost(0)
223 , attacks_inflicted()
224 , defends_inflicted()
225 , attacks_taken()
226 , defends_taken()
227 , damage_inflicted(0)
228 , damage_taken(0)
229 , turn_damage_inflicted(0)
230 , turn_damage_taken(0)
231 , by_cth_inflicted()
232 , by_cth_taken()
233 , turn_by_cth_inflicted()
234 , turn_by_cth_taken()
235 , expected_damage_inflicted(0)
236 , expected_damage_taken(0)
237 , turn_expected_damage_inflicted(0)
238 , turn_expected_damage_taken(0)
239 , save_id()
240{
241 read(cfg);
242}
243
245{
246 config res;
247 res.add_child("recruits", write_str_int_map(recruits));
248 res.add_child("recalls", write_str_int_map(recalls));
249 res.add_child("advances", write_str_int_map(advanced_to));
250 res.add_child("deaths", write_str_int_map(deaths));
251 res.add_child("killed", write_str_int_map(killed));
254 res.add_child("attacks_taken", write_battle_result_map(attacks_taken));
255 res.add_child("defends_taken", write_battle_result_map(defends_taken));
256 // Don't serialize by_cth_inflicted / by_cth_taken; they're deserialized from attacks_inflicted/defends_inflicted.
257 res.add_child("turn_by_cth_inflicted", write_by_cth_map(turn_by_cth_inflicted));
258 res.add_child("turn_by_cth_taken", write_by_cth_map(turn_by_cth_taken));
259
260 res["recruit_cost"] = recruit_cost;
261 res["recall_cost"] = recall_cost;
262
263 res["damage_inflicted"] = damage_inflicted;
264 res["damage_taken"] = damage_taken;
265 res["expected_damage_inflicted"] = expected_damage_inflicted;
266 res["expected_damage_taken"] = expected_damage_taken;
267
268 res["turn_damage_inflicted"] = turn_damage_inflicted;
269 res["turn_damage_taken"] = turn_damage_taken;
270 res["turn_expected_damage_inflicted"] = turn_expected_damage_inflicted;
271 res["turn_expected_damage_taken"] = turn_expected_damage_taken;
272
273 res["save_id"] = save_id;
274
275 return res;
276}
277
279{
280 out.open_child("recruits");
282 out.close_child("recruits");
283 out.open_child("recalls");
285 out.close_child("recalls");
286 out.open_child("advances");
288 out.close_child("advances");
289 out.open_child("deaths");
291 out.close_child("deaths");
292 out.open_child("killed");
294 out.close_child("killed");
295 out.open_child("attacks");
297 out.close_child("attacks");
298 out.open_child("defends");
300 out.close_child("defends");
301 out.open_child("attacks_taken");
303 out.close_child("attacks_taken");
304 out.open_child("defends_taken");
306 out.close_child("defends_taken");
307 // Don't serialize by_cth_inflicted / by_cth_taken; they're deserialized from attacks_inflicted/defends.
308 out.open_child("turn_by_cth_inflicted");
310 out.close_child("turn_by_cth_inflicted");
311 out.open_child("turn_by_cth_taken");
313 out.close_child("turn_by_cth_taken");
314
315 out.write_key_val("recruit_cost", recruit_cost);
316 out.write_key_val("recall_cost", recall_cost);
317
318 out.write_key_val("damage_inflicted", damage_inflicted);
319 out.write_key_val("damage_taken", damage_taken);
320 out.write_key_val("expected_damage_inflicted", expected_damage_inflicted);
321 out.write_key_val("expected_damage_taken", expected_damage_taken);
322
323 out.write_key_val("turn_damage_inflicted", turn_damage_inflicted);
324 out.write_key_val("turn_damage_taken", turn_damage_taken);
325 out.write_key_val("turn_expected_damage_inflicted", turn_expected_damage_inflicted);
326 out.write_key_val("turn_expected_damage_taken", turn_expected_damage_taken);
327
328 out.write_key_val("save_id", save_id);
329}
330
332{
333 if(const auto c = cfg.optional_child("recruits")) {
334 recruits = read_str_int_map(c.value());
335 }
336 if(const auto c = cfg.optional_child("recalls")) {
337 recalls = read_str_int_map(c.value());
338 }
339 if(const auto c = cfg.optional_child("advances")) {
340 advanced_to = read_str_int_map(c.value());
341 }
342 if(const auto c = cfg.optional_child("deaths")) {
343 deaths = read_str_int_map(c.value());
344 }
345 if(const auto c = cfg.optional_child("killed")) {
346 killed = read_str_int_map(c.value());
347 }
348 if(const auto c = cfg.optional_child("recalls")) {
349 recalls = read_str_int_map(c.value());
350 }
351 if(const auto c = cfg.optional_child("attacks")) {
353 }
354 if(const auto c = cfg.optional_child("defends")) {
356 }
357 if(const auto c = cfg.optional_child("attacks_taken")) {
359 }
360 if(const auto c = cfg.optional_child("defends_taken")) {
362 }
364 // by_cth_taken will be an empty map in old (pre-#4070) savefiles that don't have
365 // [attacks_taken]/[defends_taken] tags in their [statistics] tags
367 if(const auto c = cfg.optional_child("turn_by_cth_inflicted")) {
369 }
370 if(const auto c = cfg.optional_child("turn_by_cth_taken")) {
372 }
373
374 recruit_cost = cfg["recruit_cost"].to_int();
375 recall_cost = cfg["recall_cost"].to_int();
376
377 damage_inflicted = cfg["damage_inflicted"].to_long_long();
378 damage_taken = cfg["damage_taken"].to_long_long();
379 expected_damage_inflicted = cfg["expected_damage_inflicted"].to_long_long();
380 expected_damage_taken = cfg["expected_damage_taken"].to_long_long();
381
382 turn_damage_inflicted = cfg["turn_damage_inflicted"].to_long_long();
383 turn_damage_taken = cfg["turn_damage_taken"].to_long_long();
384 turn_expected_damage_inflicted = cfg["turn_expected_damage_inflicted"].to_long_long();
385 turn_expected_damage_taken = cfg["turn_expected_damage_taken"].to_long_long();
386
387 save_id = cfg["save_id"].str();
388}
389
391{
392 stats_t& a = *this;
393 DBG_NG << "Merging statistics";
394 merge_str_int_map(a.recruits, b.recruits);
395 merge_str_int_map(a.recalls, b.recalls);
396 merge_str_int_map(a.advanced_to, b.advanced_to);
397 merge_str_int_map(a.deaths, b.deaths);
398 merge_str_int_map(a.killed, b.killed);
399
400 merge_cth_map(a.by_cth_inflicted, b.by_cth_inflicted);
401 merge_cth_map(a.by_cth_taken, b.by_cth_taken);
402
403 merge_battle_result_maps(a.attacks_inflicted, b.attacks_inflicted);
404 merge_battle_result_maps(a.defends_inflicted, b.defends_inflicted);
405 merge_battle_result_maps(a.attacks_taken, b.attacks_taken);
406 merge_battle_result_maps(a.defends_taken, b.defends_taken);
407
408 a.recruit_cost += b.recruit_cost;
409 a.recall_cost += b.recall_cost;
410
411 a.damage_inflicted += b.damage_inflicted;
412 a.damage_taken += b.damage_taken;
413 a.expected_damage_inflicted += b.expected_damage_inflicted;
414 a.expected_damage_taken += b.expected_damage_taken;
415 // Only take the last value for this turn
416 a.turn_damage_inflicted = b.turn_damage_inflicted;
417 a.turn_damage_taken = b.turn_damage_taken;
418 a.turn_expected_damage_inflicted = b.turn_expected_damage_inflicted;
419 a.turn_expected_damage_taken = b.turn_expected_damage_taken;
420 a.turn_by_cth_inflicted = b.turn_by_cth_inflicted;
421 a.turn_by_cth_taken = b.turn_by_cth_taken;
422}
423
424
426 : team_stats()
427 , scenario_name(cfg["scenario"])
428{
429 for(const config& team : cfg.child_range("team")) {
430 team_stats[team["save_id"]] = stats_t(team);
431 }
432}
433
435{
436 config res;
437 res["scenario"] = scenario_name;
438 for(team_stats_t::const_iterator i = team_stats.begin(); i != team_stats.end(); ++i) {
439 res.add_child("team", i->second.write());
440 }
441
442 return res;
443}
444
446{
447 out.write_key_val("scenario", scenario_name);
448 for(team_stats_t::const_iterator i = team_stats.begin(); i != team_stats.end(); ++i) {
449 out.open_child("team");
450 i->second.write(out);
451 out.close_child("team");
452 }
453}
454
456{
457 return config("hits", hits, "strikes", strikes);
458}
459
461 : strikes(cfg["strikes"].to_int())
462 , hits(cfg["hits"].to_int())
463{
464}
465
467{
468 config res;
469
470 for(std::vector<scenario_stats_t>::const_iterator i = master_record.begin(); i != master_record.end(); ++i) {
471 res.add_child("scenario", i->write());
472 }
473
474 return res;
475}
476
478{
479 for(std::vector<scenario_stats_t>::const_iterator i = master_record.begin(); i != master_record.end(); ++i) {
480 out.open_child("scenario");
481 i->write(out);
482 out.close_child("scenario");
483 }
484}
485
486void campaign_stats_t::read(const config& cfg, bool append)
487{
488 if(!append) {
489 master_record.clear();
490 }
491 for(const config& s : cfg.child_range("scenario")) {
492 master_record.emplace_back(s);
493 }
494}
495
496void campaign_stats_t::new_scenario(const std::string& name)
497{
498 master_record.emplace_back(name);
499}
500
502{
503 if(master_record.empty() == false) {
504 master_record.back().team_stats.clear();
505 }
506}
507
508} // namespace statistics_record
509
510std::ostream& operator<<(std::ostream& outstream, const statistics_record::stats_t::hitrate_t& by_cth)
511{
512 outstream << "[" << by_cth.hits << "/" << by_cth.strikes << "]";
513 return outstream;
514}
std::ostream & operator<<(std::ostream &s, const ai::attack_result &r)
Definition actions.cpp:1118
#define ERR_NG
#define DBG_NG
Class for writing a config out to a file in pieces.
void close_child(const std::string &key)
void write_key_val(const std::string &key, const T &value)
This template function will work with any type that can be assigned to an attribute_value.
void open_child(const std::string &key)
void write(const config &cfg, bool strong_quotes=false)
A config object defines a single node in a WML file, with access to child nodes.
Definition config.hpp:157
void remove_attribute(std::string_view key)
Definition config.cpp:162
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
const_attr_itors attribute_range() const
Definition config.cpp:740
child_itors child_range(std::string_view key)
Definition config.cpp:268
bool has_attribute(std::string_view key) const
Definition config.cpp:157
This class stores all the data for a single 'side' (in game nomenclature).
Definition team.hpp:74
const config * cfg
Standard logging facilities (interface).
static stats_t::hitrate_map read_by_cth_map_from_battle_result_maps(const stats_t::battle_result_map &attacks, const stats_t::battle_result_map &defends)
static config write_by_cth_map(const stats_t::hitrate_map &m)
static stats_t::hitrate_map read_by_cth_map(const config &cfg)
static stats_t::str_int_map read_str_int_map(const config &cfg)
static stats_t::battle_result_map read_battle_result_map(const config &cfg)
static void merge_battle_result_maps(stats_t::battle_result_map &a, const stats_t::battle_result_map &b)
static config write_battle_result_map(const stats_t::battle_result_map &m)
static void merge_cth_map(stats_t::hitrate_map &a, const stats_t::hitrate_map &b)
static config write_str_int_map(const stats_t::str_int_map &m)
static void merge_str_int_map(stats_t::str_int_map &a, const stats_t::str_int_map &b)
std::string join(const Range &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::vector< std::string > split(const config_attribute_value &val)
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
static lg::log_domain log_engine("engine")
void new_scenario(const std::string &scenario_name)
Adds an entry for anew scenario to wrte to.
void read(const config &cfg, bool append=false)
void clear_current_scenario()
Delete the current scenario from the stats.
void write(config_writer &out) const
scenario_stats_t(const std::string &name)
void merge_with(const stats_t &other)
battle_result_map attacks_inflicted
Statistics of this side's attacks on its own turns.
battle_result_map defends_inflicted
Statistics of this side's attacks on enemies' turns.
std::map< int, battle_sequence_frequency_map > battle_result_map
A type that will map different % chances to hit to different results.
void read(const config &cfg)
std::map< int, hitrate_t > hitrate_map
A type that maps chance-to-hit percentage to number of hits and strikes at that CTH.
std::map< std::string, int > str_int_map
battle_result_map attacks_taken
Statistics of enemies' counter attacks on this side's turns.
battle_result_map defends_taken
Statistics of enemies' attacks against this side on their turns.
mock_party p
static map_location::direction n
static map_location::direction s
#define i
#define c
#define b