The Battle for Wesnoth  1.17.17+dev
statistics_record.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2023
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 #include <cmath>
26 
27 static lg::log_domain log_engine("engine");
28 #define DBG_NG LOG_STREAM(debug, log_engine)
29 #define ERR_NG LOG_STREAM(err, log_engine)
30 
32 {
33 
35 {
36  config res;
37  for(stats_t::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
38  std::string n = std::to_string(i->second);
39  if(res.has_attribute(n)) {
40  res[n] = res[n].str() + "," + i->first;
41  } else {
42  res[n] = i->first;
43  }
44  }
45 
46  return res;
47 }
48 
50 {
51  using reverse_map = std::multimap<int, std::string>;
52  reverse_map rev;
53  std::transform(m.begin(), m.end(), std::inserter(rev, rev.begin()),
54  [](const stats_t::str_int_map::value_type p) { return std::pair(p.second, p.first); });
55  reverse_map::const_iterator i = rev.begin(), j;
56  while(i != rev.end()) {
57  j = rev.upper_bound(i->first);
58  std::vector<std::string> vals;
59  std::transform(i, j, std::back_inserter(vals), [](const reverse_map::value_type& p) { return p.second; });
60  out.write_key_val(std::to_string(i->first), utils::join(vals));
61  i = j;
62  }
63 }
64 
66 {
68  for(const config::attribute& i : cfg.attribute_range()) {
69  try {
70  for(const std::string& val : utils::split(i.second)) {
71  m[val] = std::stoi(i.first);
72  }
73  } catch(const std::invalid_argument&) {
74  ERR_NG << "Invalid statistics entry; skipping";
75  }
76  }
77 
78  return m;
79 }
80 
82 {
83  config res;
84  for(stats_t::battle_result_map::const_iterator i = m.begin(); i != m.end(); ++i) {
85  config& new_cfg = res.add_child("sequence");
86  new_cfg = write_str_int_map(i->second);
87  new_cfg["_num"] = i->first;
88  }
89 
90  return res;
91 }
92 
94 {
95  for(stats_t::battle_result_map::const_iterator i = m.begin(); i != m.end(); ++i) {
96  out.open_child("sequence");
97  write_str_int_map(out, i->second);
98  out.write_key_val("_num", i->first);
99  out.close_child("sequence");
100  }
101 }
102 
104 {
106  for(const config& i : cfg.child_range("sequence")) {
107  config item = i;
108  int key = item["_num"];
109  item.remove_attribute("_num");
110  m[key] = read_str_int_map(item);
111  }
112 
113  return m;
114 }
115 
117 {
118  config res;
119  for(const auto& i : m) {
120  res.add_child("hitrate_map_entry", config{"cth", i.first, "stats", i.second.write()});
121  }
122  return res;
123 }
124 
126 
128  const stats_t::battle_result_map& attacks, const stats_t::battle_result_map& defends)
129 {
131 
132  stats_t::battle_result_map merged = attacks;
133  merge_battle_result_maps(merged, defends);
134 
135  for(const auto& i : merged) {
136  int cth = i.first;
137  const stats_t::battle_sequence_frequency_map& frequency_map = i.second;
138  for(const auto& j : frequency_map) {
139  const std::string& res = j.first; // see attack_context::~attack_context()
140  const int occurrences = j.second;
141  unsigned int misses = std::count(res.begin(), res.end(), '0');
142  unsigned int hits = std::count(res.begin(), res.end(), '1');
143  if(misses + hits == 0) {
144  continue;
145  }
146  misses *= occurrences;
147  hits *= occurrences;
148  m[cth].strikes += misses + hits;
149  m[cth].hits += hits;
150  }
151  }
152 
153  return m;
154 }
155 
157 {
159  for(const config& i : cfg.child_range("hitrate_map_entry")) {
160  m.emplace(i["cth"], stats_t::hitrate_t(i.mandatory_child("stats")));
161  }
162  return m;
163 }
164 
166 {
167  for(stats_t::str_int_map::const_iterator i = b.begin(); i != b.end(); ++i) {
168  a[i->first] += i->second;
169  }
170 }
171 
173 {
174  for(stats_t::battle_result_map::const_iterator i = b.begin(); i != b.end(); ++i) {
175  merge_str_int_map(a[i->first], i->second);
176  }
177 }
178 
180 {
181  for(const auto& i : b) {
182  a[i.first].hits += i.second.hits;
183  a[i.first].strikes += i.second.strikes;
184  }
185 }
186 
187 
189  : recruits()
190  , recalls()
191  , advanced_to()
192  , deaths()
193  , killed()
194  , recruit_cost(0)
195  , recall_cost(0)
196  , attacks_inflicted()
197  , defends_inflicted()
198  , attacks_taken()
199  , defends_taken()
200  , damage_inflicted(0)
201  , damage_taken(0)
202  , turn_damage_inflicted(0)
203  , turn_damage_taken(0)
204  , by_cth_inflicted()
205  , by_cth_taken()
206  , turn_by_cth_inflicted()
207  , turn_by_cth_taken()
208  , expected_damage_inflicted(0)
209  , expected_damage_taken(0)
210  , turn_expected_damage_inflicted(0)
211  , turn_expected_damage_taken(0)
212  , save_id()
213 {
214 }
215 
217  : recruits()
218  , recalls()
219  , advanced_to()
220  , deaths()
221  , killed()
222  , recruit_cost(0)
223  , recall_cost(0)
224  , attacks_inflicted()
225  , defends_inflicted()
226  , attacks_taken()
227  , defends_taken()
228  , damage_inflicted(0)
229  , damage_taken(0)
230  , turn_damage_inflicted(0)
231  , turn_damage_taken(0)
232  , by_cth_inflicted()
233  , by_cth_taken()
234  , turn_by_cth_inflicted()
235  , turn_by_cth_taken()
236  , expected_damage_inflicted(0)
237  , expected_damage_taken(0)
238  , turn_expected_damage_inflicted(0)
239  , turn_expected_damage_taken(0)
240  , save_id()
241 {
242  read(cfg);
243 }
244 
246 {
247  config res;
248  res.add_child("recruits", write_str_int_map(recruits));
249  res.add_child("recalls", write_str_int_map(recalls));
250  res.add_child("advances", write_str_int_map(advanced_to));
251  res.add_child("deaths", write_str_int_map(deaths));
252  res.add_child("killed", write_str_int_map(killed));
255  res.add_child("attacks_taken", write_battle_result_map(attacks_taken));
256  res.add_child("defends_taken", write_battle_result_map(defends_taken));
257  // Don't serialize by_cth_inflicted / by_cth_taken; they're deserialized from attacks_inflicted/defends_inflicted.
258  res.add_child("turn_by_cth_inflicted", write_by_cth_map(turn_by_cth_inflicted));
259  res.add_child("turn_by_cth_taken", write_by_cth_map(turn_by_cth_taken));
260 
261  res["recruit_cost"] = recruit_cost;
262  res["recall_cost"] = recall_cost;
263 
264  res["damage_inflicted"] = damage_inflicted;
265  res["damage_taken"] = damage_taken;
266  res["expected_damage_inflicted"] = expected_damage_inflicted;
267  res["expected_damage_taken"] = expected_damage_taken;
268 
269  res["turn_damage_inflicted"] = turn_damage_inflicted;
270  res["turn_damage_taken"] = turn_damage_taken;
271  res["turn_expected_damage_inflicted"] = turn_expected_damage_inflicted;
272  res["turn_expected_damage_taken"] = turn_expected_damage_taken;
273 
274  res["save_id"] = save_id;
275 
276  return res;
277 }
278 
280 {
281  out.open_child("recruits");
283  out.close_child("recruits");
284  out.open_child("recalls");
286  out.close_child("recalls");
287  out.open_child("advances");
289  out.close_child("advances");
290  out.open_child("deaths");
292  out.close_child("deaths");
293  out.open_child("killed");
295  out.close_child("killed");
296  out.open_child("attacks");
298  out.close_child("attacks");
299  out.open_child("defends");
301  out.close_child("defends");
302  out.open_child("attacks_taken");
304  out.close_child("attacks_taken");
305  out.open_child("defends_taken");
307  out.close_child("defends_taken");
308  // Don't serialize by_cth_inflicted / by_cth_taken; they're deserialized from attacks_inflicted/defends.
309  out.open_child("turn_by_cth_inflicted");
311  out.close_child("turn_by_cth_inflicted");
312  out.open_child("turn_by_cth_taken");
314  out.close_child("turn_by_cth_taken");
315 
316  out.write_key_val("recruit_cost", recruit_cost);
317  out.write_key_val("recall_cost", recall_cost);
318 
319  out.write_key_val("damage_inflicted", damage_inflicted);
320  out.write_key_val("damage_taken", damage_taken);
321  out.write_key_val("expected_damage_inflicted", expected_damage_inflicted);
322  out.write_key_val("expected_damage_taken", expected_damage_taken);
323 
324  out.write_key_val("turn_damage_inflicted", turn_damage_inflicted);
325  out.write_key_val("turn_damage_taken", turn_damage_taken);
326  out.write_key_val("turn_expected_damage_inflicted", turn_expected_damage_inflicted);
327  out.write_key_val("turn_expected_damage_taken", turn_expected_damage_taken);
328 
329  out.write_key_val("save_id", save_id);
330 }
331 
332 void stats_t::read(const config& cfg)
333 {
334  if(const auto c = cfg.optional_child("recruits")) {
335  recruits = read_str_int_map(c.value());
336  }
337  if(const auto c = cfg.optional_child("recalls")) {
338  recalls = read_str_int_map(c.value());
339  }
340  if(const auto c = cfg.optional_child("advances")) {
341  advanced_to = read_str_int_map(c.value());
342  }
343  if(const auto c = cfg.optional_child("deaths")) {
344  deaths = read_str_int_map(c.value());
345  }
346  if(const auto c = cfg.optional_child("killed")) {
347  killed = read_str_int_map(c.value());
348  }
349  if(const auto c = cfg.optional_child("recalls")) {
350  recalls = read_str_int_map(c.value());
351  }
352  if(const auto c = cfg.optional_child("attacks")) {
354  }
355  if(const auto c = cfg.optional_child("defends")) {
357  }
358  if(const auto c = cfg.optional_child("attacks_taken")) {
360  }
361  if(const auto c = cfg.optional_child("defends_taken")) {
363  }
365  // by_cth_taken will be an empty map in old (pre-#4070) savefiles that don't have
366  // [attacks_taken]/[defends_taken] tags in their [statistics] tags
368  if(const auto c = cfg.optional_child("turn_by_cth_inflicted")) {
370  }
371  if(const auto c = cfg.optional_child("turn_by_cth_taken")) {
373  }
374 
375  recruit_cost = cfg["recruit_cost"].to_int();
376  recall_cost = cfg["recall_cost"].to_int();
377 
378  damage_inflicted = cfg["damage_inflicted"].to_long_long();
379  damage_taken = cfg["damage_taken"].to_long_long();
380  expected_damage_inflicted = cfg["expected_damage_inflicted"].to_long_long();
381  expected_damage_taken = cfg["expected_damage_taken"].to_long_long();
382 
383  turn_damage_inflicted = cfg["turn_damage_inflicted"].to_long_long();
384  turn_damage_taken = cfg["turn_damage_taken"].to_long_long();
385  turn_expected_damage_inflicted = cfg["turn_expected_damage_inflicted"].to_long_long();
386  turn_expected_damage_taken = cfg["turn_expected_damage_taken"].to_long_long();
387 
388  save_id = cfg["save_id"].str();
389 }
390 
392 {
393  stats_t& a = *this;
394  DBG_NG << "Merging statistics";
395  merge_str_int_map(a.recruits, b.recruits);
396  merge_str_int_map(a.recalls, b.recalls);
397  merge_str_int_map(a.advanced_to, b.advanced_to);
398  merge_str_int_map(a.deaths, b.deaths);
399  merge_str_int_map(a.killed, b.killed);
400 
401  merge_cth_map(a.by_cth_inflicted, b.by_cth_inflicted);
402  merge_cth_map(a.by_cth_taken, b.by_cth_taken);
403 
404  merge_battle_result_maps(a.attacks_inflicted, b.attacks_inflicted);
405  merge_battle_result_maps(a.defends_inflicted, b.defends_inflicted);
406  merge_battle_result_maps(a.attacks_taken, b.attacks_taken);
407  merge_battle_result_maps(a.defends_taken, b.defends_taken);
408 
409  a.recruit_cost += b.recruit_cost;
410  a.recall_cost += b.recall_cost;
411 
412  a.damage_inflicted += b.damage_inflicted;
413  a.damage_taken += b.damage_taken;
414  a.expected_damage_inflicted += b.expected_damage_inflicted;
415  a.expected_damage_taken += b.expected_damage_taken;
416  // Only take the last value for this turn
417  a.turn_damage_inflicted = b.turn_damage_inflicted;
418  a.turn_damage_taken = b.turn_damage_taken;
419  a.turn_expected_damage_inflicted = b.turn_expected_damage_inflicted;
420  a.turn_expected_damage_taken = b.turn_expected_damage_taken;
421  a.turn_by_cth_inflicted = b.turn_by_cth_inflicted;
422  a.turn_by_cth_taken = b.turn_by_cth_taken;
423 }
424 
425 
427  : team_stats()
428  , scenario_name(cfg["scenario"])
429 {
430  for(const config& team : cfg.child_range("team")) {
431  team_stats[team["save_id"]] = stats_t(team);
432  }
433 }
434 
436 {
437  config res;
438  res["scenario"] = scenario_name;
439  for(team_stats_t::const_iterator i = team_stats.begin(); i != team_stats.end(); ++i) {
440  res.add_child("team", i->second.write());
441  }
442 
443  return res;
444 }
445 
447 {
448  out.write_key_val("scenario", scenario_name);
449  for(team_stats_t::const_iterator i = team_stats.begin(); i != team_stats.end(); ++i) {
450  out.open_child("team");
451  i->second.write(out);
452  out.close_child("team");
453  }
454 }
455 
457 {
458  return config("hits", hits, "strikes", strikes);
459 }
460 
462  : strikes(cfg["strikes"])
463  , hits(cfg["hits"])
464 {
465 }
466 
468 {
469  config res;
470 
471  for(std::vector<scenario_stats_t>::const_iterator i = master_record.begin(); i != master_record.end(); ++i) {
472  res.add_child("scenario", i->write());
473  }
474 
475  return res;
476 }
477 
479 {
480  for(std::vector<scenario_stats_t>::const_iterator i = master_record.begin(); i != master_record.end(); ++i) {
481  out.open_child("scenario");
482  i->write(out);
483  out.close_child("scenario");
484  }
485 }
486 
487 void campaign_stats_t::read(const config& cfg, bool append)
488 {
489  if(!append) {
490  master_record.clear();
491  }
492  for(const config& s : cfg.child_range("scenario")) {
493  master_record.emplace_back(s);
494  }
495 }
496 
497 void campaign_stats_t::new_scenario(const std::string& name)
498 {
499  master_record.emplace_back(name);
500 }
501 
503 {
504  if(master_record.empty() == false) {
505  master_record.back().team_stats.clear();
506  }
507 }
508 
509 } // namespace statistics_record
510 
511 std::ostream& operator<<(std::ostream& outstream, const statistics_record::stats_t::hitrate_t& by_cth)
512 {
513  outstream << "[" << by_cth.hits << "/" << by_cth.strikes << "]";
514  return outstream;
515 }
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:161
const_attr_itors attribute_range() const
Definition: config.cpp:767
bool has_attribute(config_key_type key) const
Definition: config.cpp:159
child_itors child_range(config_key_type key)
Definition: config.cpp:277
attribute_map::value_type attribute
Definition: config.hpp:301
optional_config_impl< config > optional_child(config_key_type key, int n=0)
Euivalent to mandatory_child, but returns an empty optional if the nth child was not found.
Definition: config.cpp:389
config & add_child(config_key_type key)
Definition: config.cpp:445
This class stores all the data for a single 'side' (in game nomenclature).
Definition: team.hpp:76
std::size_t i
Definition: function.cpp:968
Standard logging facilities (interface).
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:414
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 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 a
#define b