841 lines
24 KiB
C++
841 lines
24 KiB
C++
/*
|
|
Copyright (C) 2003 - 2018 by David White <dave@whitevine.net>
|
|
Part of the Battle for Wesnoth Project https://www.wesnoth.org/
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY.
|
|
|
|
See the COPYING file for more details.
|
|
*/
|
|
|
|
/**
|
|
* @file
|
|
* Manage statistics: recruitments, recalls, kills, losses, etc.
|
|
*/
|
|
|
|
#include "game_board.hpp"
|
|
#include "statistics.hpp"
|
|
#include "log.hpp"
|
|
#include "resources.hpp" // Needed for teams, to get team save_id for a unit
|
|
#include "serialization/binary_or_text.hpp"
|
|
#include "team.hpp" // Needed to get team save_id
|
|
#include "units/unit.hpp"
|
|
|
|
static lg::log_domain log_engine("engine");
|
|
#define DBG_NG LOG_STREAM(debug, log_engine)
|
|
#define ERR_NG LOG_STREAM(err, log_engine)
|
|
|
|
namespace {
|
|
|
|
// This variable is true whenever the statistics are mid-scenario.
|
|
// This means a new scenario shouldn't be added to the master stats record.
|
|
bool mid_scenario = false;
|
|
|
|
typedef statistics::stats stats;
|
|
typedef std::map<std::string,stats> team_stats_t;
|
|
|
|
std::string get_team_save_id(const unit & u)
|
|
{
|
|
assert(resources::gameboard);
|
|
return resources::gameboard->get_team(u.side()).save_id_or_number();
|
|
}
|
|
|
|
struct scenario_stats
|
|
{
|
|
explicit scenario_stats(const std::string& name) :
|
|
team_stats(),
|
|
scenario_name(name)
|
|
{}
|
|
|
|
explicit scenario_stats(const config& cfg);
|
|
|
|
config write() const;
|
|
void write(config_writer &out) const;
|
|
|
|
team_stats_t team_stats;
|
|
std::string scenario_name;
|
|
};
|
|
|
|
scenario_stats::scenario_stats(const config& cfg) :
|
|
team_stats(),
|
|
scenario_name(cfg["scenario"])
|
|
{
|
|
for(const config &team : cfg.child_range("team")) {
|
|
team_stats[team["save_id"]] = stats(team);
|
|
}
|
|
}
|
|
|
|
config scenario_stats::write() const
|
|
{
|
|
config res;
|
|
res["scenario"] = scenario_name;
|
|
for(team_stats_t::const_iterator i = team_stats.begin(); i != team_stats.end(); ++i) {
|
|
res.add_child("team",i->second.write());
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void scenario_stats::write(config_writer &out) const
|
|
{
|
|
out.write_key_val("scenario", scenario_name);
|
|
for(team_stats_t::const_iterator i = team_stats.begin(); i != team_stats.end(); ++i) {
|
|
out.open_child("team");
|
|
i->second.write(out);
|
|
out.close_child("team");
|
|
}
|
|
}
|
|
|
|
std::vector<scenario_stats> master_stats;
|
|
|
|
} // end anon namespace
|
|
|
|
static stats &get_stats(const std::string &save_id)
|
|
{
|
|
if(master_stats.empty()) {
|
|
master_stats.emplace_back(std::string());
|
|
}
|
|
|
|
team_stats_t& team_stats = master_stats.back().team_stats;
|
|
return team_stats[save_id];
|
|
}
|
|
|
|
static config write_str_int_map(const stats::str_int_map& m)
|
|
{
|
|
config res;
|
|
for(stats::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
|
|
std::string n = std::to_string(i->second);
|
|
if(res.has_attribute(n)) {
|
|
res[n] = res[n].str() + "," + i->first;
|
|
} else {
|
|
res[n] = i->first;
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static void write_str_int_map(config_writer &out, const stats::str_int_map& m)
|
|
{
|
|
using reverse_map = std::multimap<int, std::string>;
|
|
reverse_map rev;
|
|
std::transform(
|
|
m.begin(), m.end(),
|
|
std::inserter(rev, rev.begin()),
|
|
[](const stats::str_int_map::value_type p) {
|
|
return std::make_pair(p.second, p.first);
|
|
}
|
|
);
|
|
reverse_map::const_iterator i = rev.begin(), j;
|
|
while(i != rev.end()) {
|
|
j = rev.upper_bound(i->first);
|
|
std::vector<std::string> vals;
|
|
std::transform(i, j, std::back_inserter(vals), [](const reverse_map::value_type& p) {
|
|
return p.second;
|
|
});
|
|
out.write_key_val(std::to_string(i->first), utils::join(vals));
|
|
i = j;
|
|
}
|
|
}
|
|
|
|
static stats::str_int_map read_str_int_map(const config& cfg)
|
|
{
|
|
stats::str_int_map m;
|
|
for(const config::attribute &i : cfg.attribute_range()) {
|
|
try {
|
|
for(const std::string& val : utils::split(i.second)) {
|
|
m[val] = std::stoi(i.first);
|
|
}
|
|
} catch(const std::invalid_argument&) {
|
|
ERR_NG << "Invalid statistics entry; skipping\n";
|
|
}
|
|
}
|
|
|
|
return m;
|
|
}
|
|
|
|
static config write_battle_result_map(const stats::battle_result_map& m)
|
|
{
|
|
config res;
|
|
for(stats::battle_result_map::const_iterator i = m.begin(); i != m.end(); ++i) {
|
|
config& new_cfg = res.add_child("sequence");
|
|
new_cfg = write_str_int_map(i->second);
|
|
new_cfg["_num"] = i->first;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
static void write_battle_result_map(config_writer &out, const stats::battle_result_map& m)
|
|
{
|
|
for(stats::battle_result_map::const_iterator i = m.begin(); i != m.end(); ++i) {
|
|
out.open_child("sequence");
|
|
write_str_int_map(out, i->second);
|
|
out.write_key_val("_num", i->first);
|
|
out.close_child("sequence");
|
|
}
|
|
}
|
|
|
|
static stats::battle_result_map read_battle_result_map(const config& cfg)
|
|
{
|
|
stats::battle_result_map m;
|
|
for(const config &i : cfg.child_range("sequence"))
|
|
{
|
|
config item = i;
|
|
int key = item["_num"];
|
|
item.remove_attribute("_num");
|
|
m[key] = read_str_int_map(item);
|
|
}
|
|
|
|
return m;
|
|
}
|
|
|
|
static config write_by_cth_map(const stats::hitrate_map& m)
|
|
{
|
|
config res;
|
|
for(const auto& i : m) {
|
|
res.add_child("hitrate_map_entry", config {
|
|
"cth", i.first,
|
|
"stats", i.second.write()
|
|
});
|
|
}
|
|
return res;
|
|
}
|
|
|
|
static void merge_battle_result_maps(stats::battle_result_map& a, const stats::battle_result_map& b);
|
|
static stats::hitrate_map read_by_cth_map_from_battle_result_maps(const statistics::stats::battle_result_map& attacks, const statistics::stats::battle_result_map& defends)
|
|
{
|
|
stats::hitrate_map m;
|
|
|
|
statistics::stats::battle_result_map merged = attacks;
|
|
merge_battle_result_maps(merged, defends);
|
|
|
|
for(const auto& i : merged) {
|
|
int cth = i.first;
|
|
const statistics::stats::battle_sequence_frequency_map& frequency_map = i.second;
|
|
for(const auto& j : frequency_map) {
|
|
const std::string& res = j.first; // see attack_context::~attack_context()
|
|
const int occurrences = j.second;
|
|
unsigned int misses = std::count(res.begin(), res.end(), '0');
|
|
unsigned int hits = std::count(res.begin(), res.end(), '1');
|
|
if(misses + hits == 0) {
|
|
continue;
|
|
}
|
|
misses *= occurrences;
|
|
hits *= occurrences;
|
|
m[cth].strikes += misses + hits;
|
|
m[cth].hits += hits;
|
|
}
|
|
}
|
|
|
|
return m;
|
|
}
|
|
|
|
static stats::hitrate_map read_by_cth_map(const config& cfg)
|
|
{
|
|
stats::hitrate_map m;
|
|
for(const config &i : cfg.child_range("hitrate_map_entry")) {
|
|
m.emplace(i["cth"], statistics::stats::hitrate_t(i.child("stats")));
|
|
}
|
|
return m;
|
|
}
|
|
|
|
static void merge_str_int_map(stats::str_int_map& a, const stats::str_int_map& b)
|
|
{
|
|
for(stats::str_int_map::const_iterator i = b.begin(); i != b.end(); ++i) {
|
|
a[i->first] += i->second;
|
|
}
|
|
}
|
|
|
|
static void merge_battle_result_maps(stats::battle_result_map& a, const stats::battle_result_map& b)
|
|
{
|
|
for(stats::battle_result_map::const_iterator i = b.begin(); i != b.end(); ++i) {
|
|
merge_str_int_map(a[i->first],i->second);
|
|
}
|
|
}
|
|
|
|
static void merge_cth_map(stats::hitrate_map& a, const stats::hitrate_map& b)
|
|
{
|
|
for(const auto& i : b) {
|
|
a[i.first].hits += i.second.hits;
|
|
a[i.first].strikes += i.second.strikes;
|
|
}
|
|
}
|
|
|
|
static void merge_stats(stats& a, const stats& b)
|
|
{
|
|
DBG_NG << "Merging statistics\n";
|
|
merge_str_int_map(a.recruits,b.recruits);
|
|
merge_str_int_map(a.recalls,b.recalls);
|
|
merge_str_int_map(a.advanced_to,b.advanced_to);
|
|
merge_str_int_map(a.deaths,b.deaths);
|
|
merge_str_int_map(a.killed,b.killed);
|
|
|
|
merge_cth_map(a.by_cth_inflicted,b.by_cth_inflicted);
|
|
merge_cth_map(a.by_cth_taken,b.by_cth_taken);
|
|
|
|
merge_battle_result_maps(a.attacks_inflicted,b.attacks_inflicted);
|
|
merge_battle_result_maps(a.defends_inflicted,b.defends_inflicted);
|
|
merge_battle_result_maps(a.attacks_taken,b.attacks_taken);
|
|
merge_battle_result_maps(a.defends_taken,b.defends_taken);
|
|
|
|
a.recruit_cost += b.recruit_cost;
|
|
a.recall_cost += b.recall_cost;
|
|
|
|
a.damage_inflicted += b.damage_inflicted;
|
|
a.damage_taken += b.damage_taken;
|
|
a.expected_damage_inflicted += b.expected_damage_inflicted;
|
|
a.expected_damage_taken += b.expected_damage_taken;
|
|
// Only take the last value for this turn
|
|
a.turn_damage_inflicted = b.turn_damage_inflicted;
|
|
a.turn_damage_taken = b.turn_damage_taken;
|
|
a.turn_expected_damage_inflicted = b.turn_expected_damage_inflicted;
|
|
a.turn_expected_damage_taken = b.turn_expected_damage_taken;
|
|
a.turn_by_cth_inflicted = b.turn_by_cth_inflicted;
|
|
a.turn_by_cth_taken = b.turn_by_cth_taken;
|
|
|
|
}
|
|
|
|
namespace statistics
|
|
{
|
|
|
|
stats::stats() :
|
|
recruits(),
|
|
recalls(),
|
|
advanced_to(),
|
|
deaths(),
|
|
killed(),
|
|
recruit_cost(0),
|
|
recall_cost(0),
|
|
attacks_inflicted(),
|
|
defends_inflicted(),
|
|
attacks_taken(),
|
|
defends_taken(),
|
|
damage_inflicted(0),
|
|
damage_taken(0),
|
|
turn_damage_inflicted(0),
|
|
turn_damage_taken(0),
|
|
by_cth_inflicted(),
|
|
by_cth_taken(),
|
|
turn_by_cth_inflicted(),
|
|
turn_by_cth_taken(),
|
|
expected_damage_inflicted(0),
|
|
expected_damage_taken(0),
|
|
turn_expected_damage_inflicted(0),
|
|
turn_expected_damage_taken(0),
|
|
save_id()
|
|
{}
|
|
|
|
stats::stats(const config& cfg) :
|
|
recruits(),
|
|
recalls(),
|
|
advanced_to(),
|
|
deaths(),
|
|
killed(),
|
|
recruit_cost(0),
|
|
recall_cost(0),
|
|
attacks_inflicted(),
|
|
defends_inflicted(),
|
|
attacks_taken(),
|
|
defends_taken(),
|
|
damage_inflicted(0),
|
|
damage_taken(0),
|
|
turn_damage_inflicted(0),
|
|
turn_damage_taken(0),
|
|
by_cth_inflicted(),
|
|
by_cth_taken(),
|
|
turn_by_cth_inflicted(),
|
|
turn_by_cth_taken(),
|
|
expected_damage_inflicted(0),
|
|
expected_damage_taken(0),
|
|
turn_expected_damage_inflicted(0),
|
|
turn_expected_damage_taken(0),
|
|
save_id()
|
|
{
|
|
read(cfg);
|
|
}
|
|
|
|
config stats::write() const
|
|
{
|
|
config res;
|
|
res.add_child("recruits",write_str_int_map(recruits));
|
|
res.add_child("recalls",write_str_int_map(recalls));
|
|
res.add_child("advances",write_str_int_map(advanced_to));
|
|
res.add_child("deaths",write_str_int_map(deaths));
|
|
res.add_child("killed",write_str_int_map(killed));
|
|
res.add_child("attacks",write_battle_result_map(attacks_inflicted));
|
|
res.add_child("defends",write_battle_result_map(defends_inflicted));
|
|
res.add_child("attacks_taken",write_battle_result_map(attacks_taken));
|
|
res.add_child("defends_taken",write_battle_result_map(defends_taken));
|
|
// Don't serialize by_cth_inflicted / by_cth_taken; they're deserialized from attacks_inflicted/defends_inflicted.
|
|
res.add_child("turn_by_cth_inflicted", write_by_cth_map(turn_by_cth_inflicted));
|
|
res.add_child("turn_by_cth_taken", write_by_cth_map(turn_by_cth_taken));
|
|
|
|
res["recruit_cost"] = recruit_cost;
|
|
res["recall_cost"] = recall_cost;
|
|
|
|
res["damage_inflicted"] = damage_inflicted;
|
|
res["damage_taken"] = damage_taken;
|
|
res["expected_damage_inflicted"] = expected_damage_inflicted;
|
|
res["expected_damage_taken"] = expected_damage_taken;
|
|
|
|
res["turn_damage_inflicted"] = turn_damage_inflicted;
|
|
res["turn_damage_taken"] = turn_damage_taken;
|
|
res["turn_expected_damage_inflicted"] = turn_expected_damage_inflicted;
|
|
res["turn_expected_damage_taken"] = turn_expected_damage_taken;
|
|
|
|
res["save_id"] = save_id;
|
|
|
|
return res;
|
|
}
|
|
|
|
void stats::write(config_writer &out) const
|
|
{
|
|
out.open_child("recruits");
|
|
write_str_int_map(out, recruits);
|
|
out.close_child("recruits");
|
|
out.open_child("recalls");
|
|
write_str_int_map(out, recalls);
|
|
out.close_child("recalls");
|
|
out.open_child("advances");
|
|
write_str_int_map(out, advanced_to);
|
|
out.close_child("advances");
|
|
out.open_child("deaths");
|
|
write_str_int_map(out, deaths);
|
|
out.close_child("deaths");
|
|
out.open_child("killed");
|
|
write_str_int_map(out, killed);
|
|
out.close_child("killed");
|
|
out.open_child("attacks");
|
|
write_battle_result_map(out, attacks_inflicted);
|
|
out.close_child("attacks");
|
|
out.open_child("defends");
|
|
write_battle_result_map(out, defends_inflicted);
|
|
out.close_child("defends");
|
|
out.open_child("attacks_taken");
|
|
write_battle_result_map(out, attacks_taken);
|
|
out.close_child("attacks_taken");
|
|
out.open_child("defends_taken");
|
|
write_battle_result_map(out, defends_taken);
|
|
out.close_child("defends_taken");
|
|
// Don't serialize by_cth_inflicted / by_cth_taken; they're deserialized from attacks_inflicted/defends.
|
|
out.open_child("turn_by_cth_inflicted");
|
|
out.write(write_by_cth_map(turn_by_cth_inflicted));
|
|
out.close_child("turn_by_cth_inflicted");
|
|
out.open_child("turn_by_cth_taken");
|
|
out.write(write_by_cth_map(turn_by_cth_taken));
|
|
out.close_child("turn_by_cth_taken");
|
|
|
|
out.write_key_val("recruit_cost", recruit_cost);
|
|
out.write_key_val("recall_cost", recall_cost);
|
|
|
|
out.write_key_val("damage_inflicted", damage_inflicted);
|
|
out.write_key_val("damage_taken", damage_taken);
|
|
out.write_key_val("expected_damage_inflicted", expected_damage_inflicted);
|
|
out.write_key_val("expected_damage_taken", expected_damage_taken);
|
|
|
|
out.write_key_val("turn_damage_inflicted", turn_damage_inflicted);
|
|
out.write_key_val("turn_damage_taken", turn_damage_taken);
|
|
out.write_key_val("turn_expected_damage_inflicted", turn_expected_damage_inflicted);
|
|
out.write_key_val("turn_expected_damage_taken", turn_expected_damage_taken);
|
|
|
|
out.write_key_val("save_id", save_id);
|
|
}
|
|
|
|
void stats::read(const config& cfg)
|
|
{
|
|
if (const config &c = cfg.child("recruits")) {
|
|
recruits = read_str_int_map(c);
|
|
}
|
|
if (const config &c = cfg.child("recalls")) {
|
|
recalls = read_str_int_map(c);
|
|
}
|
|
if (const config &c = cfg.child("advances")) {
|
|
advanced_to = read_str_int_map(c);
|
|
}
|
|
if (const config &c = cfg.child("deaths")) {
|
|
deaths = read_str_int_map(c);
|
|
}
|
|
if (const config &c = cfg.child("killed")) {
|
|
killed = read_str_int_map(c);
|
|
}
|
|
if (const config &c = cfg.child("recalls")) {
|
|
recalls = read_str_int_map(c);
|
|
}
|
|
if (const config &c = cfg.child("attacks")) {
|
|
attacks_inflicted = read_battle_result_map(c);
|
|
}
|
|
if (const config &c = cfg.child("defends")) {
|
|
defends_inflicted = read_battle_result_map(c);
|
|
}
|
|
if (const config &c = cfg.child("attacks_taken")) {
|
|
attacks_taken = read_battle_result_map(c);
|
|
}
|
|
if (const config &c = cfg.child("defends_taken")) {
|
|
defends_taken = read_battle_result_map(c);
|
|
}
|
|
by_cth_inflicted = read_by_cth_map_from_battle_result_maps(attacks_inflicted, defends_inflicted);
|
|
// by_cth_taken will be an empty map in old (pre-#4070) savefiles that don't have
|
|
// [attacks_taken]/[defends_taken] tags in their [statistics] tags
|
|
by_cth_taken = read_by_cth_map_from_battle_result_maps(attacks_taken, defends_taken);
|
|
if (const config &c = cfg.child("turn_by_cth_inflicted")) {
|
|
turn_by_cth_inflicted = read_by_cth_map(c);
|
|
}
|
|
if (const config &c = cfg.child("turn_by_cth_taken")) {
|
|
turn_by_cth_taken = read_by_cth_map(c);
|
|
}
|
|
|
|
recruit_cost = cfg["recruit_cost"].to_int();
|
|
recall_cost = cfg["recall_cost"].to_int();
|
|
|
|
damage_inflicted = cfg["damage_inflicted"].to_long_long();
|
|
damage_taken = cfg["damage_taken"].to_long_long();
|
|
expected_damage_inflicted = cfg["expected_damage_inflicted"].to_long_long();
|
|
expected_damage_taken = cfg["expected_damage_taken"].to_long_long();
|
|
|
|
turn_damage_inflicted = cfg["turn_damage_inflicted"].to_long_long();
|
|
turn_damage_taken = cfg["turn_damage_taken"].to_long_long();
|
|
turn_expected_damage_inflicted = cfg["turn_expected_damage_inflicted"].to_long_long();
|
|
turn_expected_damage_taken = cfg["turn_expected_damage_taken"].to_long_long();
|
|
|
|
save_id = cfg["save_id"].str();
|
|
}
|
|
|
|
scenario_context::scenario_context(const std::string& name)
|
|
{
|
|
if(!mid_scenario || master_stats.empty()) {
|
|
master_stats.emplace_back(name);
|
|
}
|
|
|
|
mid_scenario = true;
|
|
}
|
|
|
|
scenario_context::~scenario_context()
|
|
{
|
|
mid_scenario = false;
|
|
}
|
|
|
|
attack_context::attack_context(const unit& a,
|
|
const unit& d, int a_cth, int d_cth) :
|
|
attacker_type(a.type_id()),
|
|
defender_type(d.type_id()),
|
|
attacker_side(get_team_save_id(a)),
|
|
defender_side(get_team_save_id(d)),
|
|
chance_to_hit_defender(a_cth),
|
|
chance_to_hit_attacker(d_cth),
|
|
attacker_res(),
|
|
defender_res()
|
|
{
|
|
}
|
|
|
|
attack_context::~attack_context()
|
|
{
|
|
std::string attacker_key = "s" + attacker_res;
|
|
std::string defender_key = "s" + defender_res;
|
|
|
|
attacker_stats().attacks_inflicted[chance_to_hit_defender][attacker_key]++;
|
|
defender_stats().defends_inflicted[chance_to_hit_attacker][defender_key]++;
|
|
|
|
attacker_stats().attacks_taken[chance_to_hit_attacker][defender_key]++;
|
|
defender_stats().defends_taken[chance_to_hit_defender][attacker_key]++;
|
|
}
|
|
|
|
stats& attack_context::attacker_stats()
|
|
{
|
|
return get_stats(attacker_side);
|
|
}
|
|
|
|
stats& attack_context::defender_stats()
|
|
{
|
|
return get_stats(defender_side);
|
|
}
|
|
|
|
void attack_context::attack_expected_damage(double attacker_inflict_, double defender_inflict_)
|
|
{
|
|
int attacker_inflict = std::round(attacker_inflict_ * stats::decimal_shift);
|
|
int defender_inflict = std::round(defender_inflict_ * stats::decimal_shift);
|
|
stats &att_stats = attacker_stats(), &def_stats = defender_stats();
|
|
att_stats.expected_damage_inflicted += attacker_inflict;
|
|
att_stats.expected_damage_taken += defender_inflict;
|
|
def_stats.expected_damage_inflicted += defender_inflict;
|
|
def_stats.expected_damage_taken += attacker_inflict;
|
|
att_stats.turn_expected_damage_inflicted += attacker_inflict;
|
|
att_stats.turn_expected_damage_taken += defender_inflict;
|
|
def_stats.turn_expected_damage_inflicted += defender_inflict;
|
|
def_stats.turn_expected_damage_taken += attacker_inflict;
|
|
}
|
|
|
|
|
|
void attack_context::attack_result(hit_result res, int cth, int damage, int drain)
|
|
{
|
|
attacker_res.push_back(res == MISSES ? '0' : '1');
|
|
stats &att_stats = attacker_stats(), &def_stats = defender_stats();
|
|
|
|
if(res != MISSES) {
|
|
++att_stats.by_cth_inflicted[cth].hits;
|
|
++att_stats.turn_by_cth_inflicted[cth].hits;
|
|
++def_stats.by_cth_taken[cth].hits;
|
|
++def_stats.turn_by_cth_taken[cth].hits;
|
|
}
|
|
++att_stats.by_cth_inflicted[cth].strikes;
|
|
++att_stats.turn_by_cth_inflicted[cth].strikes;
|
|
++def_stats.by_cth_taken[cth].strikes;
|
|
++def_stats.turn_by_cth_taken[cth].strikes;
|
|
|
|
if(res != MISSES) {
|
|
// handle drain
|
|
att_stats.damage_taken -= drain;
|
|
def_stats.damage_inflicted -= drain;
|
|
att_stats.turn_damage_taken -= drain;
|
|
def_stats.turn_damage_inflicted -= drain;
|
|
|
|
att_stats.damage_inflicted += damage;
|
|
def_stats.damage_taken += damage;
|
|
att_stats.turn_damage_inflicted += damage;
|
|
def_stats.turn_damage_taken += damage;
|
|
}
|
|
|
|
if(res == KILLS) {
|
|
++att_stats.killed[defender_type];
|
|
++def_stats.deaths[defender_type];
|
|
}
|
|
}
|
|
|
|
void attack_context::defend_result(hit_result res, int cth, int damage, int drain)
|
|
{
|
|
defender_res.push_back(res == MISSES ? '0' : '1');
|
|
stats &att_stats = attacker_stats(), &def_stats = defender_stats();
|
|
|
|
if(res != MISSES) {
|
|
++def_stats.by_cth_inflicted[cth].hits;
|
|
++def_stats.turn_by_cth_inflicted[cth].hits;
|
|
++att_stats.by_cth_taken[cth].hits;
|
|
++att_stats.turn_by_cth_taken[cth].hits;
|
|
}
|
|
++def_stats.by_cth_inflicted[cth].strikes;
|
|
++def_stats.turn_by_cth_inflicted[cth].strikes;
|
|
++att_stats.by_cth_taken[cth].strikes;
|
|
++att_stats.turn_by_cth_taken[cth].strikes;
|
|
|
|
if(res != MISSES) {
|
|
//handle drain
|
|
def_stats.damage_taken -= drain;
|
|
att_stats.damage_inflicted -= drain;
|
|
def_stats.turn_damage_taken -= drain;
|
|
att_stats.turn_damage_inflicted -= drain;
|
|
|
|
att_stats.damage_taken += damage;
|
|
def_stats.damage_inflicted += damage;
|
|
att_stats.turn_damage_taken += damage;
|
|
def_stats.turn_damage_inflicted += damage;
|
|
}
|
|
|
|
if(res == KILLS) {
|
|
++att_stats.deaths[attacker_type];
|
|
++def_stats.killed[attacker_type];
|
|
}
|
|
}
|
|
|
|
void recruit_unit(const unit& u)
|
|
{
|
|
stats& s = get_stats(get_team_save_id(u));
|
|
s.recruits[u.type().base_id()]++;
|
|
s.recruit_cost += u.cost();
|
|
}
|
|
|
|
void recall_unit(const unit& u)
|
|
{
|
|
stats& s = get_stats(get_team_save_id(u));
|
|
s.recalls[u.type_id()]++;
|
|
s.recall_cost += u.cost();
|
|
}
|
|
|
|
void un_recall_unit(const unit& u)
|
|
{
|
|
stats& s = get_stats(get_team_save_id(u));
|
|
s.recalls[u.type_id()]--;
|
|
s.recall_cost -= u.cost();
|
|
}
|
|
|
|
void un_recruit_unit(const unit& u)
|
|
{
|
|
stats& s = get_stats(get_team_save_id(u));
|
|
s.recruits[u.type().base_id()]--;
|
|
s.recruit_cost -= u.cost();
|
|
}
|
|
|
|
int un_recall_unit_cost(const unit& u) // this really belongs elsewhere, perhaps in undo.cpp
|
|
{ // but I'm too lazy to do it at the moment
|
|
return u.recall_cost();
|
|
}
|
|
|
|
|
|
void advance_unit(const unit& u)
|
|
{
|
|
stats& s = get_stats(get_team_save_id(u));
|
|
s.advanced_to[u.type_id()]++;
|
|
}
|
|
|
|
void reset_turn_stats(const std::string & save_id)
|
|
{
|
|
stats &s = get_stats(save_id);
|
|
s.turn_damage_inflicted = 0;
|
|
s.turn_damage_taken = 0;
|
|
s.turn_expected_damage_inflicted = 0;
|
|
s.turn_expected_damage_taken = 0;
|
|
s.turn_by_cth_inflicted.clear();
|
|
s.turn_by_cth_taken.clear();
|
|
s.save_id = save_id;
|
|
}
|
|
|
|
stats calculate_stats(const std::string & save_id)
|
|
{
|
|
stats res;
|
|
|
|
DBG_NG << "calculate_stats, side: " << save_id << " master_stats.size: " << master_stats.size() << "\n";
|
|
// The order of this loop matters since the turn stats are taken from the
|
|
// last stats merged.
|
|
for ( std::size_t i = 0; i != master_stats.size(); ++i ) {
|
|
team_stats_t::const_iterator find_it = master_stats[i].team_stats.find(save_id);
|
|
if ( find_it != master_stats[i].team_stats.end() )
|
|
merge_stats(res, find_it->second);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
|
|
/**
|
|
* Returns a list of names and stats for each scenario in the current campaign.
|
|
* The front of the list is the oldest scenario; the back of the list is the
|
|
* (most) current scenario.
|
|
* Only scenarios with stats for the given @a side_id are included, but if no
|
|
* scenarios are applicable, then a vector containing a single dummy entry will
|
|
* be returned. (I.e., this never returns an empty vector.)
|
|
* This list is intended for the statistics dialog and may become invalid if
|
|
* new stats are recorded.
|
|
*/
|
|
levels level_stats(const std::string & save_id)
|
|
{
|
|
static const stats null_stats;
|
|
static const std::string null_name("");
|
|
|
|
levels level_list;
|
|
|
|
for ( std::size_t level = 0; level != master_stats.size(); ++level ) {
|
|
const team_stats_t & team_stats = master_stats[level].team_stats;
|
|
|
|
team_stats_t::const_iterator find_it = team_stats.find(save_id);
|
|
if ( find_it != team_stats.end() )
|
|
level_list.emplace_back(&master_stats[level].scenario_name, &find_it->second);
|
|
}
|
|
|
|
// Make sure we do return something (so other code does not have to deal
|
|
// with an empty list).
|
|
if ( level_list.empty() )
|
|
level_list.emplace_back(&null_name, &null_stats);
|
|
|
|
return level_list;
|
|
}
|
|
|
|
|
|
config write_stats()
|
|
{
|
|
config res;
|
|
res["mid_scenario"] = mid_scenario;
|
|
|
|
for(std::vector<scenario_stats>::const_iterator i = master_stats.begin(); i != master_stats.end(); ++i) {
|
|
res.add_child("scenario",i->write());
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void write_stats(config_writer &out)
|
|
{
|
|
out.write_key_val("mid_scenario", mid_scenario);
|
|
|
|
for(std::vector<scenario_stats>::const_iterator i = master_stats.begin(); i != master_stats.end(); ++i) {
|
|
out.open_child("scenario");
|
|
i->write(out);
|
|
out.close_child("scenario");
|
|
}
|
|
}
|
|
|
|
void read_stats(const config& cfg)
|
|
{
|
|
fresh_stats();
|
|
mid_scenario = cfg["mid_scenario"].to_bool();
|
|
|
|
for(const config &s : cfg.child_range("scenario")) {
|
|
master_stats.emplace_back(s);
|
|
}
|
|
}
|
|
|
|
void fresh_stats()
|
|
{
|
|
master_stats.clear();
|
|
mid_scenario = false;
|
|
}
|
|
|
|
void clear_current_scenario()
|
|
{
|
|
if(master_stats.empty() == false) {
|
|
master_stats.pop_back();
|
|
mid_scenario = false;
|
|
}
|
|
}
|
|
|
|
void reset_current_scenario()
|
|
{
|
|
assert(!master_stats.empty());
|
|
master_stats.back().team_stats = {};
|
|
mid_scenario = false;
|
|
}
|
|
|
|
int sum_str_int_map(const stats::str_int_map& m)
|
|
{
|
|
int res = 0;
|
|
for(stats::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
|
|
res += i->second;
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
int sum_cost_str_int_map(const stats::str_int_map &m)
|
|
{
|
|
int cost = 0;
|
|
for (stats::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
|
|
const unit_type *t = unit_types.find(i->first);
|
|
if (!t) {
|
|
ERR_NG << "Statistics refer to unknown unit type '" << i->first << "'. Discarding." << std::endl;
|
|
} else {
|
|
cost += i->second * t->cost();
|
|
}
|
|
}
|
|
|
|
return cost;
|
|
}
|
|
|
|
config stats::hitrate_t::write() const
|
|
{
|
|
return config("hits", hits, "strikes", strikes);
|
|
}
|
|
|
|
stats::hitrate_t::hitrate_t(const config &cfg) :
|
|
strikes(cfg["strikes"]),
|
|
hits(cfg["hits"])
|
|
{}
|
|
|
|
} // end namespace statistics
|
|
|
|
std::ostream& operator<<(std::ostream& outstream, const statistics::stats::hitrate_t& by_cth) {
|
|
outstream << "[" << by_cth.hits << "/" << by_cth.strikes << "]";
|
|
return outstream;
|
|
}
|
|
|