The Battle for Wesnoth  1.19.7+dev
statistics_record.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2023 - 2024
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 
26 static 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 
331 void stats_t::read(const config& cfg)
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 
486 void 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 
496 void 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 
510 std::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 }
Class for writing a config out to a file in pieces.
void close_child(const std::string &key)
void write(const config &cfg)
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)
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
const_attr_itors attribute_range() const
Definition: config.cpp:760
bool has_attribute(config_key_type key) const
Definition: config.cpp:157
child_itors child_range(config_key_type key)
Definition: config.cpp:272
void remove_attribute(config_key_type key)
Definition: config.cpp:162
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Equivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:384
config & add_child(config_key_type key)
Definition: config.cpp:440
This class stores all the data for a single 'side' (in game nomenclature).
Definition: team.hpp:75
std::size_t i
Definition: function.cpp:1029
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)
int stoi(std::string_view str)
Same interface as std::stoi and meant as a drop in replacement, except:
Definition: charconv.hpp:154
std::string join(const T &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)
static lg::log_domain log_engine("engine")
#define ERR_NG
std::ostream & operator<<(std::ostream &outstream, const statistics_record::stats_t::hitrate_t &by_cth)
#define DBG_NG
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_char c
mock_party p
static map_location::direction n
static map_location::direction s
#define b