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