1 /*
2 Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
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: recruitments, recalls, kills, losses, etc.
18 */
19
20 #include "game_board.hpp"
21 #include "statistics.hpp"
22 #include "log.hpp"
23 #include "resources.hpp" // Needed for teams, to get team save_id for a unit
24 #include "serialization/binary_or_text.hpp"
25 #include "team.hpp" // Needed to get team save_id
26 #include "units/unit.hpp"
27
28 static lg::log_domain log_engine("engine");
29 #define DBG_NG LOG_STREAM(debug, log_engine)
30 #define ERR_NG LOG_STREAM(err, log_engine)
31
32 namespace {
33
34 // This variable is true whenever the statistics are mid-scenario.
35 // This means a new scenario shouldn't be added to the master stats record.
36 bool mid_scenario = false;
37
38 typedef statistics::stats stats;
39 typedef std::map<std::string,stats> team_stats_t;
40
get_team_save_id(const unit & u)41 std::string get_team_save_id(const unit & u)
42 {
43 assert(resources::gameboard);
44 return resources::gameboard->get_team(u.side()).save_id_or_number();
45 }
46
47 struct scenario_stats
48 {
scenario_stats__anon1bd4de2e0111::scenario_stats49 explicit scenario_stats(const std::string& name) :
50 team_stats(),
51 scenario_name(name)
52 {}
53
54 explicit scenario_stats(const config& cfg);
55
56 config write() const;
57 void write(config_writer &out) const;
58
59 team_stats_t team_stats;
60 std::string scenario_name;
61 };
62
scenario_stats(const config & cfg)63 scenario_stats::scenario_stats(const config& cfg) :
64 team_stats(),
65 scenario_name(cfg["scenario"])
66 {
67 for(const config &team : cfg.child_range("team")) {
68 team_stats[team["save_id"]] = stats(team);
69 }
70 }
71
write() const72 config scenario_stats::write() const
73 {
74 config res;
75 res["scenario"] = scenario_name;
76 for(team_stats_t::const_iterator i = team_stats.begin(); i != team_stats.end(); ++i) {
77 res.add_child("team",i->second.write());
78 }
79
80 return res;
81 }
82
write(config_writer & out) const83 void scenario_stats::write(config_writer &out) const
84 {
85 out.write_key_val("scenario", scenario_name);
86 for(team_stats_t::const_iterator i = team_stats.begin(); i != team_stats.end(); ++i) {
87 out.open_child("team");
88 i->second.write(out);
89 out.close_child("team");
90 }
91 }
92
93 std::vector<scenario_stats> master_stats;
94
95 } // end anon namespace
96
get_stats(const std::string & save_id)97 static stats &get_stats(const std::string &save_id)
98 {
99 if(master_stats.empty()) {
100 master_stats.emplace_back(std::string());
101 }
102
103 team_stats_t& team_stats = master_stats.back().team_stats;
104 return team_stats[save_id];
105 }
106
write_str_int_map(const stats::str_int_map & m)107 static config write_str_int_map(const stats::str_int_map& m)
108 {
109 config res;
110 for(stats::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
111 std::string n = std::to_string(i->second);
112 if(res.has_attribute(n)) {
113 res[n] = res[n].str() + "," + i->first;
114 } else {
115 res[n] = i->first;
116 }
117 }
118
119 return res;
120 }
121
write_str_int_map(config_writer & out,const stats::str_int_map & m)122 static void write_str_int_map(config_writer &out, const stats::str_int_map& m)
123 {
124 using reverse_map = std::multimap<int, std::string>;
125 reverse_map rev;
126 std::transform(
127 m.begin(), m.end(),
128 std::inserter(rev, rev.begin()),
129 [](const stats::str_int_map::value_type p) {
130 return std::make_pair(p.second, p.first);
131 }
132 );
133 reverse_map::const_iterator i = rev.begin(), j;
134 while(i != rev.end()) {
135 j = rev.upper_bound(i->first);
136 std::vector<std::string> vals;
137 std::transform(i, j, std::back_inserter(vals), [](const reverse_map::value_type& p) {
138 return p.second;
139 });
140 out.write_key_val(std::to_string(i->first), utils::join(vals));
141 i = j;
142 }
143 }
144
read_str_int_map(const config & cfg)145 static stats::str_int_map read_str_int_map(const config& cfg)
146 {
147 stats::str_int_map m;
148 for(const config::attribute &i : cfg.attribute_range()) {
149 try {
150 for(const std::string& val : utils::split(i.second)) {
151 m[val] = std::stoi(i.first);
152 }
153 } catch(const std::invalid_argument&) {
154 ERR_NG << "Invalid statistics entry; skipping\n";
155 }
156 }
157
158 return m;
159 }
160
write_battle_result_map(const stats::battle_result_map & m)161 static config write_battle_result_map(const stats::battle_result_map& m)
162 {
163 config res;
164 for(stats::battle_result_map::const_iterator i = m.begin(); i != m.end(); ++i) {
165 config& new_cfg = res.add_child("sequence");
166 new_cfg = write_str_int_map(i->second);
167 new_cfg["_num"] = i->first;
168 }
169
170 return res;
171 }
172
write_battle_result_map(config_writer & out,const stats::battle_result_map & m)173 static void write_battle_result_map(config_writer &out, const stats::battle_result_map& m)
174 {
175 for(stats::battle_result_map::const_iterator i = m.begin(); i != m.end(); ++i) {
176 out.open_child("sequence");
177 write_str_int_map(out, i->second);
178 out.write_key_val("_num", i->first);
179 out.close_child("sequence");
180 }
181 }
182
read_battle_result_map(const config & cfg)183 static stats::battle_result_map read_battle_result_map(const config& cfg)
184 {
185 stats::battle_result_map m;
186 for(const config &i : cfg.child_range("sequence"))
187 {
188 config item = i;
189 int key = item["_num"];
190 item.remove_attribute("_num");
191 m[key] = read_str_int_map(item);
192 }
193
194 return m;
195 }
196
merge_str_int_map(stats::str_int_map & a,const stats::str_int_map & b)197 static void merge_str_int_map(stats::str_int_map& a, const stats::str_int_map& b)
198 {
199 for(stats::str_int_map::const_iterator i = b.begin(); i != b.end(); ++i) {
200 a[i->first] += i->second;
201 }
202 }
203
merge_battle_result_maps(stats::battle_result_map & a,const stats::battle_result_map & b)204 static void merge_battle_result_maps(stats::battle_result_map& a, const stats::battle_result_map& b)
205 {
206 for(stats::battle_result_map::const_iterator i = b.begin(); i != b.end(); ++i) {
207 merge_str_int_map(a[i->first],i->second);
208 }
209 }
210
merge_stats(stats & a,const stats & b)211 static void merge_stats(stats& a, const stats& b)
212 {
213 DBG_NG << "Merging statistics\n";
214 merge_str_int_map(a.recruits,b.recruits);
215 merge_str_int_map(a.recalls,b.recalls);
216 merge_str_int_map(a.advanced_to,b.advanced_to);
217 merge_str_int_map(a.deaths,b.deaths);
218 merge_str_int_map(a.killed,b.killed);
219
220 merge_battle_result_maps(a.attacks,b.attacks);
221 merge_battle_result_maps(a.defends,b.defends);
222
223 a.recruit_cost += b.recruit_cost;
224 a.recall_cost += b.recall_cost;
225
226 a.damage_inflicted += b.damage_inflicted;
227 a.damage_taken += b.damage_taken;
228 a.expected_damage_inflicted += b.expected_damage_inflicted;
229 a.expected_damage_taken += b.expected_damage_taken;
230 // Only take the last value for this turn
231 a.turn_damage_inflicted = b.turn_damage_inflicted;
232 a.turn_damage_taken = b.turn_damage_taken;
233 a.turn_expected_damage_inflicted = b.turn_expected_damage_inflicted;
234 a.turn_expected_damage_taken = b.turn_expected_damage_taken;
235 }
236
237 namespace statistics
238 {
239
stats()240 stats::stats() :
241 recruits(),
242 recalls(),
243 advanced_to(),
244 deaths(),
245 killed(),
246 recruit_cost(0),
247 recall_cost(0),
248 attacks(),
249 defends(),
250 damage_inflicted(0),
251 damage_taken(0),
252 turn_damage_inflicted(0),
253 turn_damage_taken(0),
254 expected_damage_inflicted(0),
255 expected_damage_taken(0),
256 turn_expected_damage_inflicted(0),
257 turn_expected_damage_taken(0),
258 save_id()
259 {}
260
stats(const config & cfg)261 stats::stats(const config& cfg) :
262 recruits(),
263 recalls(),
264 advanced_to(),
265 deaths(),
266 killed(),
267 recruit_cost(0),
268 recall_cost(0),
269 attacks(),
270 defends(),
271 damage_inflicted(0),
272 damage_taken(0),
273 turn_damage_inflicted(0),
274 turn_damage_taken(0),
275 expected_damage_inflicted(0),
276 expected_damage_taken(0),
277 turn_expected_damage_inflicted(0),
278 turn_expected_damage_taken(0),
279 save_id()
280 {
281 read(cfg);
282 }
283
write() const284 config stats::write() const
285 {
286 config res;
287 res.add_child("recruits",write_str_int_map(recruits));
288 res.add_child("recalls",write_str_int_map(recalls));
289 res.add_child("advances",write_str_int_map(advanced_to));
290 res.add_child("deaths",write_str_int_map(deaths));
291 res.add_child("killed",write_str_int_map(killed));
292 res.add_child("attacks",write_battle_result_map(attacks));
293 res.add_child("defends",write_battle_result_map(defends));
294
295 res["recruit_cost"] = recruit_cost;
296 res["recall_cost"] = recall_cost;
297
298 res["damage_inflicted"] = damage_inflicted;
299 res["damage_taken"] = damage_taken;
300 res["expected_damage_inflicted"] = expected_damage_inflicted;
301 res["expected_damage_taken"] = expected_damage_taken;
302
303 res["turn_damage_inflicted"] = turn_damage_inflicted;
304 res["turn_damage_taken"] = turn_damage_taken;
305 res["turn_expected_damage_inflicted"] = turn_expected_damage_inflicted;
306 res["turn_expected_damage_taken"] = turn_expected_damage_taken;
307
308 res["save_id"] = save_id;
309
310 return res;
311 }
312
write(config_writer & out) const313 void stats::write(config_writer &out) const
314 {
315 out.open_child("recruits");
316 write_str_int_map(out, recruits);
317 out.close_child("recruits");
318 out.open_child("recalls");
319 write_str_int_map(out, recalls);
320 out.close_child("recalls");
321 out.open_child("advances");
322 write_str_int_map(out, advanced_to);
323 out.close_child("advances");
324 out.open_child("deaths");
325 write_str_int_map(out, deaths);
326 out.close_child("deaths");
327 out.open_child("killed");
328 write_str_int_map(out, killed);
329 out.close_child("killed");
330 out.open_child("attacks");
331 write_battle_result_map(out, attacks);
332 out.close_child("attacks");
333 out.open_child("defends");
334 write_battle_result_map(out, defends);
335 out.close_child("defends");
336
337 out.write_key_val("recruit_cost", recruit_cost);
338 out.write_key_val("recall_cost", recall_cost);
339
340 out.write_key_val("damage_inflicted", damage_inflicted);
341 out.write_key_val("damage_taken", damage_taken);
342 out.write_key_val("expected_damage_inflicted", expected_damage_inflicted);
343 out.write_key_val("expected_damage_taken", expected_damage_taken);
344
345 out.write_key_val("turn_damage_inflicted", turn_damage_inflicted);
346 out.write_key_val("turn_damage_taken", turn_damage_taken);
347 out.write_key_val("turn_expected_damage_inflicted", turn_expected_damage_inflicted);
348 out.write_key_val("turn_expected_damage_taken", turn_expected_damage_taken);
349
350 out.write_key_val("save_id", save_id);
351 }
352
read(const config & cfg)353 void stats::read(const config& cfg)
354 {
355 if (const config &c = cfg.child("recruits")) {
356 recruits = read_str_int_map(c);
357 }
358 if (const config &c = cfg.child("recalls")) {
359 recalls = read_str_int_map(c);
360 }
361 if (const config &c = cfg.child("advances")) {
362 advanced_to = read_str_int_map(c);
363 }
364 if (const config &c = cfg.child("deaths")) {
365 deaths = read_str_int_map(c);
366 }
367 if (const config &c = cfg.child("killed")) {
368 killed = read_str_int_map(c);
369 }
370 if (const config &c = cfg.child("recalls")) {
371 recalls = read_str_int_map(c);
372 }
373 if (const config &c = cfg.child("attacks")) {
374 attacks = read_battle_result_map(c);
375 }
376 if (const config &c = cfg.child("defends")) {
377 defends = read_battle_result_map(c);
378 }
379
380 recruit_cost = cfg["recruit_cost"].to_int();
381 recall_cost = cfg["recall_cost"].to_int();
382
383 damage_inflicted = cfg["damage_inflicted"].to_long_long();
384 damage_taken = cfg["damage_taken"].to_long_long();
385 expected_damage_inflicted = cfg["expected_damage_inflicted"].to_long_long();
386 expected_damage_taken = cfg["expected_damage_taken"].to_long_long();
387
388 turn_damage_inflicted = cfg["turn_damage_inflicted"].to_long_long();
389 turn_damage_taken = cfg["turn_damage_taken"].to_long_long();
390 turn_expected_damage_inflicted = cfg["turn_expected_damage_inflicted"].to_long_long();
391 turn_expected_damage_taken = cfg["turn_expected_damage_taken"].to_long_long();
392
393 save_id = cfg["save_id"].str();
394 }
395
scenario_context(const std::string & name)396 scenario_context::scenario_context(const std::string& name)
397 {
398 if(!mid_scenario || master_stats.empty()) {
399 master_stats.emplace_back(name);
400 }
401
402 mid_scenario = true;
403 }
404
~scenario_context()405 scenario_context::~scenario_context()
406 {
407 mid_scenario = false;
408 }
409
attack_context(const unit & a,const unit & d,int a_cth,int d_cth)410 attack_context::attack_context(const unit& a,
411 const unit& d, int a_cth, int d_cth) :
412 attacker_type(a.type_id()),
413 defender_type(d.type_id()),
414 attacker_side(get_team_save_id(a)),
415 defender_side(get_team_save_id(d)),
416 chance_to_hit_defender(a_cth),
417 chance_to_hit_attacker(d_cth),
418 attacker_res(),
419 defender_res()
420 {
421 }
422
~attack_context()423 attack_context::~attack_context()
424 {
425 std::string attacker_key = "s" + attacker_res;
426 std::string defender_key = "s" + defender_res;
427
428 attacker_stats().attacks[chance_to_hit_defender][attacker_key]++;
429 defender_stats().defends[chance_to_hit_attacker][defender_key]++;
430 }
431
attacker_stats()432 stats& attack_context::attacker_stats()
433 {
434 return get_stats(attacker_side);
435 }
436
defender_stats()437 stats& attack_context::defender_stats()
438 {
439 return get_stats(defender_side);
440 }
441
attack_expected_damage(double attacker_inflict_,double defender_inflict_)442 void attack_context::attack_expected_damage(double attacker_inflict_, double defender_inflict_)
443 {
444 int attacker_inflict = round_double(attacker_inflict_ * stats::decimal_shift);
445 int defender_inflict = round_double(defender_inflict_ * stats::decimal_shift);
446 stats &att_stats = attacker_stats(), &def_stats = defender_stats();
447 att_stats.expected_damage_inflicted += attacker_inflict;
448 att_stats.expected_damage_taken += defender_inflict;
449 def_stats.expected_damage_inflicted += defender_inflict;
450 def_stats.expected_damage_taken += attacker_inflict;
451 att_stats.turn_expected_damage_inflicted += attacker_inflict;
452 att_stats.turn_expected_damage_taken += defender_inflict;
453 def_stats.turn_expected_damage_inflicted += defender_inflict;
454 def_stats.turn_expected_damage_taken += attacker_inflict;
455 }
456
457
attack_result(hit_result res,int damage,int drain)458 void attack_context::attack_result(hit_result res, int damage, int drain)
459 {
460 attacker_res.push_back(res == MISSES ? '0' : '1');
461 stats &att_stats = attacker_stats(), &def_stats = defender_stats();
462
463 if(res != MISSES) {
464 // handle drain
465 att_stats.damage_taken -= drain;
466 def_stats.damage_inflicted -= drain;
467 att_stats.turn_damage_taken -= drain;
468 def_stats.turn_damage_inflicted -= drain;
469
470 att_stats.damage_inflicted += damage;
471 def_stats.damage_taken += damage;
472 att_stats.turn_damage_inflicted += damage;
473 def_stats.turn_damage_taken += damage;
474 }
475
476 if(res == KILLS) {
477 ++att_stats.killed[defender_type];
478 ++def_stats.deaths[defender_type];
479 }
480 }
481
defend_result(hit_result res,int damage,int drain)482 void attack_context::defend_result(hit_result res, int damage, int drain)
483 {
484 defender_res.push_back(res == MISSES ? '0' : '1');
485 stats &att_stats = attacker_stats(), &def_stats = defender_stats();
486
487 if(res != MISSES) {
488 //handle drain
489 def_stats.damage_taken -= drain;
490 att_stats.damage_inflicted -= drain;
491 def_stats.turn_damage_taken -= drain;
492 att_stats.turn_damage_inflicted -= drain;
493
494 att_stats.damage_taken += damage;
495 def_stats.damage_inflicted += damage;
496 att_stats.turn_damage_taken += damage;
497 def_stats.turn_damage_inflicted += damage;
498 }
499
500 if(res == KILLS) {
501 ++att_stats.deaths[attacker_type];
502 ++def_stats.killed[attacker_type];
503 }
504 }
505
recruit_unit(const unit & u)506 void recruit_unit(const unit& u)
507 {
508 stats& s = get_stats(get_team_save_id(u));
509 s.recruits[u.type().base_id()]++;
510 s.recruit_cost += u.cost();
511 }
512
recall_unit(const unit & u)513 void recall_unit(const unit& u)
514 {
515 stats& s = get_stats(get_team_save_id(u));
516 s.recalls[u.type_id()]++;
517 s.recall_cost += u.cost();
518 }
519
un_recall_unit(const unit & u)520 void un_recall_unit(const unit& u)
521 {
522 stats& s = get_stats(get_team_save_id(u));
523 s.recalls[u.type_id()]--;
524 s.recall_cost -= u.cost();
525 }
526
un_recruit_unit(const unit & u)527 void un_recruit_unit(const unit& u)
528 {
529 stats& s = get_stats(get_team_save_id(u));
530 s.recruits[u.type().base_id()]--;
531 s.recruit_cost -= u.cost();
532 }
533
un_recall_unit_cost(const unit & u)534 int un_recall_unit_cost(const unit& u) // this really belongs elsewhere, perhaps in undo.cpp
535 { // but I'm too lazy to do it at the moment
536 return u.recall_cost();
537 }
538
539
advance_unit(const unit & u)540 void advance_unit(const unit& u)
541 {
542 stats& s = get_stats(get_team_save_id(u));
543 s.advanced_to[u.type_id()]++;
544 }
545
reset_turn_stats(const std::string & save_id)546 void reset_turn_stats(const std::string & save_id)
547 {
548 stats &s = get_stats(save_id);
549 s.turn_damage_inflicted = 0;
550 s.turn_damage_taken = 0;
551 s.turn_expected_damage_inflicted = 0;
552 s.turn_expected_damage_taken = 0;
553 s.save_id = save_id;
554 }
555
calculate_stats(const std::string & save_id)556 stats calculate_stats(const std::string & save_id)
557 {
558 stats res;
559
560 DBG_NG << "calculate_stats, side: " << save_id << " master_stats.size: " << master_stats.size() << "\n";
561 // The order of this loop matters since the turn stats are taken from the
562 // last stats merged.
563 for ( size_t i = 0; i != master_stats.size(); ++i ) {
564 team_stats_t::const_iterator find_it = master_stats[i].team_stats.find(save_id);
565 if ( find_it != master_stats[i].team_stats.end() )
566 merge_stats(res, find_it->second);
567 }
568
569 return res;
570 }
571
572
573 /**
574 * Returns a list of names and stats for each scenario in the current campaign.
575 * The front of the list is the oldest scenario; the back of the list is the
576 * (most) current scenario.
577 * Only scenarios with stats for the given @a side_id are included, but if no
578 * scenarios are applicable, then a vector containing a single dummy entry will
579 * be returned. (I.e., this never returns an empty vector.)
580 * This list is intended for the statistics dialog and may become invalid if
581 * new stats are recorded.
582 */
level_stats(const std::string & save_id)583 levels level_stats(const std::string & save_id)
584 {
585 static const stats null_stats;
586 static const std::string null_name("");
587
588 levels level_list;
589
590 for ( size_t level = 0; level != master_stats.size(); ++level ) {
591 const team_stats_t & team_stats = master_stats[level].team_stats;
592
593 team_stats_t::const_iterator find_it = team_stats.find(save_id);
594 if ( find_it != team_stats.end() )
595 level_list.emplace_back(&master_stats[level].scenario_name, &find_it->second);
596 }
597
598 // Make sure we do return something (so other code does not have to deal
599 // with an empty list).
600 if ( level_list.empty() )
601 level_list.emplace_back(&null_name, &null_stats);
602
603 return level_list;
604 }
605
606
write_stats()607 config write_stats()
608 {
609 config res;
610 res["mid_scenario"] = mid_scenario;
611
612 for(std::vector<scenario_stats>::const_iterator i = master_stats.begin(); i != master_stats.end(); ++i) {
613 res.add_child("scenario",i->write());
614 }
615
616 return res;
617 }
618
write_stats(config_writer & out)619 void write_stats(config_writer &out)
620 {
621 out.write_key_val("mid_scenario", mid_scenario ? "yes" : "no");
622
623 for(std::vector<scenario_stats>::const_iterator i = master_stats.begin(); i != master_stats.end(); ++i) {
624 out.open_child("scenario");
625 i->write(out);
626 out.close_child("scenario");
627 }
628 }
629
read_stats(const config & cfg)630 void read_stats(const config& cfg)
631 {
632 fresh_stats();
633 mid_scenario = cfg["mid_scenario"].to_bool();
634
635 for(const config &s : cfg.child_range("scenario")) {
636 master_stats.emplace_back(s);
637 }
638 }
639
fresh_stats()640 void fresh_stats()
641 {
642 master_stats.clear();
643 mid_scenario = false;
644 }
645
clear_current_scenario()646 void clear_current_scenario()
647 {
648 if(master_stats.empty() == false) {
649 master_stats.pop_back();
650 mid_scenario = false;
651 }
652 }
653
reset_current_scenario()654 void reset_current_scenario()
655 {
656 assert(!master_stats.empty());
657 master_stats.back().team_stats = team_stats_t{};
658 mid_scenario = false;
659 }
660
sum_str_int_map(const stats::str_int_map & m)661 int sum_str_int_map(const stats::str_int_map& m)
662 {
663 int res = 0;
664 for(stats::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
665 res += i->second;
666 }
667
668 return res;
669 }
670
sum_cost_str_int_map(const stats::str_int_map & m)671 int sum_cost_str_int_map(const stats::str_int_map &m)
672 {
673 int cost = 0;
674 for (stats::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
675 const unit_type *t = unit_types.find(i->first);
676 if (!t) {
677 ERR_NG << "Statistics refer to unknown unit type '" << i->first << "'. Discarding." << std::endl;
678 } else {
679 cost += i->second * t->cost();
680 }
681 }
682
683 return cost;
684 }
685
686 } // end namespace statistics
687