Refactor statistics
Previously statistics were stored in global variables, now it is a part of saved_game. With this saved_game now finally represent the contents of a safefile as it was intended to, without needing to fill the statistics part in some global variable. (See also #4672 ) In particular now no longer have to manually reset the statistics as random parts of the code, it gets reset along with the saved_game object. Also it is now in theroy possible for multiple saved_game objects to exist. Statistics was split in two objects, the statistics_record which only contains the data, and statistics_t, which provides methods to modify statistics during a game (to get cleaner dependencies) This fixes multiple related bugs with statistics in replays: - #4133 (stats not bring reset when loading a replay) - #4133 (duplicate entry for current scenario in replay) - #4441 (wrong stats at the beginning of a replay) And issues with statistics being lost for non-host players when reloading a game in (online) mp (no ticket for that one found).
This commit is contained in:
parent
b567b4286c
commit
01f28b12ae
29 changed files with 849 additions and 790 deletions
|
@ -1157,6 +1157,8 @@
|
|||
<Unit filename="../../src/spirit_po/version.hpp" />
|
||||
<Unit filename="../../src/statistics.cpp" />
|
||||
<Unit filename="../../src/statistics.hpp" />
|
||||
<Unit filename="../../src/statistics_record.cpp" />
|
||||
<Unit filename="../../src/statistics_record.hpp" />
|
||||
<Unit filename="../../src/storyscreen/controller.cpp" />
|
||||
<Unit filename="../../src/storyscreen/controller.hpp" />
|
||||
<Unit filename="../../src/storyscreen/parser.cpp" />
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
1234567890ABCDEF12345679 /* file_progress.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 1234567890ABCDEF12345680 /* file_progress.cpp */; };
|
||||
36B146FAA79A55E9F43723B1 /* general.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 84234C54BB84519421FD4136 /* general.cpp */; };
|
||||
3C254DF5B7DF196F2041955F /* mp_report.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 58C649488B3014E6F7254B62 /* mp_report.cpp */; };
|
||||
3E9A4297B4A2828C569C8927 /* statistics_record.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27764FB68F02032F1C0B6748 /* statistics_record.cpp */; };
|
||||
4291489DA38012477DA3BA7C /* general.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 84234C54BB84519421FD4136 /* general.cpp */; };
|
||||
460CA6D52143362800B89741 /* apple_version.mm in Sources */ = {isa = PBXBuildFile; fileRef = 46F54C26211DFB7200374A1C /* apple_version.mm */; };
|
||||
460D898624DC7831000B1ABC /* game.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 460D897824DC7830000B1ABC /* game.cpp */; };
|
||||
|
@ -642,6 +643,7 @@
|
|||
62D24F2F1519982500350848 /* editor_toolkit.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 62D24F2B1519982500350848 /* editor_toolkit.cpp */; };
|
||||
62D24F321519987400350848 /* context_manager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 62D24F311519987400350848 /* context_manager.cpp */; };
|
||||
62D24F351519995200350848 /* palette_manager.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 62D24F341519995200350848 /* palette_manager.cpp */; };
|
||||
6D574EACA3483ABEE72819F0 /* statistics_record.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 27764FB68F02032F1C0B6748 /* statistics_record.cpp */; };
|
||||
867141839BDB89BFE876E310 /* carryover_show_gold.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 09A440B1A671C45BE2924FB4 /* carryover_show_gold.cpp */; };
|
||||
87744447951D17AA38BE5F48 /* mp_report.cpp in Sources */ = {isa = PBXBuildFile; fileRef = 58C649488B3014E6F7254B62 /* mp_report.cpp */; };
|
||||
8D11072F0486CEB800E47090 /* Cocoa.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1058C7A1FEA54F0111CA2CBB /* Cocoa.framework */; };
|
||||
|
@ -1520,6 +1522,7 @@
|
|||
1234567890ABCDEF12345680 /* file_progress.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; path = file_progress.cpp; sourceTree = "<group>"; };
|
||||
1234567890ABCDEF12345681 /* file_progress.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = file_progress.hpp; sourceTree = "<group>"; };
|
||||
1C58BBDF21822A930078D25A /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
|
||||
27764FB68F02032F1C0B6748 /* statistics_record.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; name = statistics_record.cpp; path = statistics_record.cpp; sourceTree = "<group>"; };
|
||||
460D897824DC7830000B1ABC /* game.cpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.cpp; name = game.cpp; path = ../../src/server/wesnothd/game.cpp; sourceTree = SOURCE_ROOT; };
|
||||
460D897924DC7830000B1ABC /* ban.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = ban.hpp; path = ../../src/server/wesnothd/ban.hpp; sourceTree = SOURCE_ROOT; };
|
||||
460D897A24DC7830000B1ABC /* player_network.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; name = player_network.hpp; path = ../../src/server/wesnothd/player_network.hpp; sourceTree = SOURCE_ROOT; };
|
||||
|
@ -3400,6 +3403,7 @@
|
|||
9197972726199F54001E8133 /* wml_separators.hpp */,
|
||||
91B621AC1B76B05700B00E0F /* xBRZ */,
|
||||
09A440B1A671C45BE2924FB4 /* carryover_show_gold.cpp */,
|
||||
27764FB68F02032F1C0B6748 /* statistics_record.cpp */,
|
||||
);
|
||||
name = src;
|
||||
path = ../../src;
|
||||
|
@ -5754,6 +5758,7 @@
|
|||
36B146FAA79A55E9F43723B1 /* general.cpp in Sources */,
|
||||
3C254DF5B7DF196F2041955F /* mp_report.cpp in Sources */,
|
||||
97714C7A9FF444E29DCEF0BA /* carryover_show_gold.cpp in Sources */,
|
||||
6D574EACA3483ABEE72819F0 /* statistics_record.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
@ -6423,6 +6428,7 @@
|
|||
4291489DA38012477DA3BA7C /* general.cpp in Sources */,
|
||||
87744447951D17AA38BE5F48 /* mp_report.cpp in Sources */,
|
||||
867141839BDB89BFE876E310 /* carryover_show_gold.cpp in Sources */,
|
||||
3E9A4297B4A2828C569C8927 /* statistics_record.cpp in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -358,6 +358,7 @@ sdl/point.cpp
|
|||
sdl/texture.cpp
|
||||
side_filter.cpp
|
||||
statistics.cpp
|
||||
statistics_record.cpp
|
||||
storyscreen/controller.cpp
|
||||
storyscreen/parser.cpp
|
||||
storyscreen/part.cpp
|
||||
|
|
|
@ -388,7 +388,7 @@ void advance_unit(map_location loc, const advancement_option &advance_to, bool f
|
|||
new_unit->set_location(loc);
|
||||
if ( !use_amla )
|
||||
{
|
||||
statistics::advance_unit(*new_unit);
|
||||
resources::controller->statistics().advance_unit(*new_unit);
|
||||
preferences::encountered_units().insert(new_unit->type_id());
|
||||
LOG_CF << "Added '" << new_unit->type_id() << "' to the encountered units.";
|
||||
}
|
||||
|
|
|
@ -713,7 +713,7 @@ private:
|
|||
{
|
||||
};
|
||||
|
||||
bool perform_hit(bool, statistics::attack_context&);
|
||||
bool perform_hit(bool, statistics_attack_context&);
|
||||
void fire_event(const std::string& n);
|
||||
void refresh_bc();
|
||||
|
||||
|
@ -957,7 +957,7 @@ void attack::refresh_bc()
|
|||
d_.damage_ = d_stats_->damage;
|
||||
}
|
||||
|
||||
bool attack::perform_hit(bool attacker_turn, statistics::attack_context& stats)
|
||||
bool attack::perform_hit(bool attacker_turn, statistics_attack_context& stats)
|
||||
{
|
||||
unit_info& attacker = attacker_turn ? a_ : d_;
|
||||
unit_info& defender = attacker_turn ? d_ : a_;
|
||||
|
@ -1090,17 +1090,17 @@ bool attack::perform_hit(bool attacker_turn, statistics::attack_context& stats)
|
|||
if(attacker_turn) {
|
||||
stats.attack_result(hits
|
||||
? (dies
|
||||
? statistics::attack_context::KILLS
|
||||
: statistics::attack_context::HITS)
|
||||
: statistics::attack_context::MISSES,
|
||||
? statistics_attack_context::KILLS
|
||||
: statistics_attack_context::HITS)
|
||||
: statistics_attack_context::MISSES,
|
||||
attacker.cth_, damage_done, drains_damage
|
||||
);
|
||||
} else {
|
||||
stats.defend_result(hits
|
||||
? (dies
|
||||
? statistics::attack_context::KILLS
|
||||
: statistics::attack_context::HITS)
|
||||
: statistics::attack_context::MISSES,
|
||||
? statistics_attack_context::KILLS
|
||||
: statistics_attack_context::HITS)
|
||||
: statistics_attack_context::MISSES,
|
||||
attacker.cth_, damage_done, drains_damage
|
||||
);
|
||||
}
|
||||
|
@ -1393,7 +1393,7 @@ void attack::perform()
|
|||
}
|
||||
|
||||
DBG_NG << "getting attack statistics";
|
||||
statistics::attack_context attack_stats(
|
||||
statistics_attack_context attack_stats(resources::controller->statistics(),
|
||||
a_.get_unit(), d_.get_unit(), a_stats_->chance_to_hit, d_stats_->chance_to_hit);
|
||||
|
||||
a_.orig_attacks_ = a_stats_->num_blows;
|
||||
|
|
|
@ -35,6 +35,7 @@
|
|||
#include "log.hpp"
|
||||
#include "map/map.hpp"
|
||||
#include "pathfind/pathfind.hpp"
|
||||
#include "play_controller.hpp"
|
||||
#include "recall_list_manager.hpp"
|
||||
#include "replay.hpp"
|
||||
#include "replay_helper.hpp"
|
||||
|
@ -722,7 +723,7 @@ void recruit_unit(const unit_type & u_type, int side_num, const map_location & l
|
|||
|
||||
// Place the recruit.
|
||||
place_recruit_result res = place_recruit(new_unit, loc, from, u_type.cost(), false, map_location::NDIRECTIONS, show);
|
||||
statistics::recruit_unit(*new_unit);
|
||||
resources::controller->statistics().recruit_unit(*new_unit);
|
||||
|
||||
// To speed things a bit, don't bother with the undo stack during
|
||||
// an AI turn. The AI will not undo nor delay shroud updates.
|
||||
|
@ -767,7 +768,7 @@ bool recall_unit(const std::string & id, team & current_team,
|
|||
res = place_recruit(recall, loc, from, recall->recall_cost(),
|
||||
true, facing, show);
|
||||
}
|
||||
statistics::recall_unit(*recall);
|
||||
resources::controller->statistics().recall_unit(*recall);
|
||||
|
||||
// To speed things a bit, don't bother with the undo stack during
|
||||
// an AI turn. The AI will not undo nor delay shroud updates.
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#include "gui/dialogs/transient_message.hpp"
|
||||
#include "game_board.hpp"
|
||||
#include "play_controller.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "team.hpp"
|
||||
#include "replay.hpp"
|
||||
|
@ -85,8 +86,8 @@ bool recall_action::undo(int side)
|
|||
return false;
|
||||
}
|
||||
|
||||
statistics::un_recall_unit(*un);
|
||||
int cost = statistics::un_recall_unit_cost(*un);
|
||||
resources::controller->statistics().un_recall_unit(*un);
|
||||
int cost = un->recall_cost();
|
||||
if (cost < 0) {
|
||||
current_team.spend_gold(-current_team.recall_cost());
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
#include "gui/dialogs/transient_message.hpp"
|
||||
#include "game_board.hpp"
|
||||
#include "play_controller.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "team.hpp"
|
||||
#include "replay.hpp"
|
||||
|
@ -81,7 +82,7 @@ bool recruit_action::undo(int side)
|
|||
}
|
||||
|
||||
const unit &un = *un_it;
|
||||
statistics::un_recruit_unit(un);
|
||||
resources::controller->statistics().un_recruit_unit(un);
|
||||
current_team.spend_gold(-un.type().cost());
|
||||
|
||||
//MP_COUNTDOWN take away recruit bonus
|
||||
|
|
|
@ -40,7 +40,6 @@
|
|||
#include "resources.hpp"
|
||||
#include "saved_game.hpp"
|
||||
#include "sound.hpp"
|
||||
#include "statistics.hpp"
|
||||
#include "utils/parse_network_address.hpp"
|
||||
#include "wesnothd_connection.hpp"
|
||||
|
||||
|
@ -617,8 +616,6 @@ void mp_manager::enter_wait_mode(int game_id, bool observe)
|
|||
// The connection should never be null here, since one should never reach this screen in local game mode.
|
||||
assert(connection);
|
||||
|
||||
statistics::fresh_stats();
|
||||
|
||||
mp_game_metadata metadata(*connection);
|
||||
metadata.is_host = false;
|
||||
|
||||
|
@ -776,8 +773,6 @@ void start_local_game_commandline(const commandline_options& cmdline_opts)
|
|||
|
||||
DBG_MP << "entering connect mode";
|
||||
|
||||
statistics::fresh_stats();
|
||||
|
||||
{
|
||||
ng::connect_engine connect_engine(state, true, nullptr);
|
||||
|
||||
|
|
|
@ -52,7 +52,6 @@
|
|||
#include "sdl/surface.hpp" // for surface
|
||||
#include "serialization/compression.hpp" // for format::NONE
|
||||
#include "serialization/string_utils.hpp" // for split
|
||||
#include "statistics.hpp"
|
||||
#include "tstring.hpp" // for operator==, operator!=
|
||||
#include "video.hpp"
|
||||
#include "wesnothd_connection_error.hpp"
|
||||
|
@ -741,12 +740,12 @@ bool game_launcher::load_game()
|
|||
play_replay_ = load.data().show_replay;
|
||||
LOG_CONFIG << "is middle game savefile: " << (state_.is_mid_game_save() ? "yes" : "no");
|
||||
LOG_CONFIG << "show replay: " << (play_replay_ ? "yes" : "no");
|
||||
// in case load.data().show_replay && !state_.is_mid_game_save()
|
||||
// in case load.data().show_replay && state_.is_start_of_scenario
|
||||
// there won't be any turns to replay, but the
|
||||
// user gets to watch the intro sequence again ...
|
||||
|
||||
if(state_.is_mid_game_save() && load.data().show_replay) {
|
||||
statistics::clear_current_scenario();
|
||||
if(!state_.is_start_of_scenario() && load.data().show_replay) {
|
||||
state_.statistics().clear_current_scenario();
|
||||
}
|
||||
|
||||
if(state_.classification().is_multiplayer()) {
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
#include "preferences/credentials.hpp"
|
||||
#include "saved_game.hpp"
|
||||
#include "side_controller.hpp"
|
||||
#include "statistics.hpp"
|
||||
#include "units/types.hpp"
|
||||
#include "utils/guard_value.hpp"
|
||||
#include "wesnothd_connection.hpp"
|
||||
|
@ -576,10 +575,6 @@ void mp_join_game::post_show(window& window)
|
|||
}
|
||||
|
||||
if(window.get_retval() == retval::OK) {
|
||||
if(auto stats = level_.optional_child("statistics")) {
|
||||
statistics::fresh_stats();
|
||||
statistics::read_stats(*stats);
|
||||
}
|
||||
|
||||
mp::level_to_gamestate(level_, state_);
|
||||
|
||||
|
|
|
@ -53,11 +53,11 @@ namespace gui2::dialogs
|
|||
{
|
||||
REGISTER_DIALOG(statistics_dialog)
|
||||
|
||||
statistics_dialog::statistics_dialog(const team& current_team)
|
||||
statistics_dialog::statistics_dialog(statistics_t& statistics, const team& current_team)
|
||||
: modal_dialog(window_id())
|
||||
, current_team_(current_team)
|
||||
, campaign_(statistics::calculate_stats(current_team.save_id_or_number()))
|
||||
, scenarios_(statistics::level_stats(current_team.save_id_or_number()))
|
||||
, campaign_(statistics.calculate_stats(current_team.save_id_or_number()))
|
||||
, scenarios_(statistics.level_stats(current_team.save_id_or_number()))
|
||||
, selection_index_(scenarios_.size()) // The extra All Scenarios menu entry makes size() a valid initial index.
|
||||
, main_stat_table_()
|
||||
{
|
||||
|
@ -101,12 +101,12 @@ void statistics_dialog::pre_show(window& window)
|
|||
update_lists();
|
||||
}
|
||||
|
||||
inline const statistics::stats& statistics_dialog::current_stats()
|
||||
inline const statistics_t::stats& statistics_dialog::current_stats()
|
||||
{
|
||||
return selection_index_ == 0 ? campaign_ : *scenarios_[selection_index_ - 1].second;
|
||||
}
|
||||
|
||||
void statistics_dialog::add_stat_row(const std::string& type, const statistics::stats::str_int_map& value, const bool has_cost)
|
||||
void statistics_dialog::add_stat_row(const std::string& type, const statistics_t::stats::str_int_map& value, const bool has_cost)
|
||||
{
|
||||
listbox& stat_list = find_widget<listbox>(get_window(), "stats_list_main", false);
|
||||
|
||||
|
@ -116,10 +116,10 @@ void statistics_dialog::add_stat_row(const std::string& type, const statistics::
|
|||
item["label"] = type;
|
||||
data.emplace("stat_type", item);
|
||||
|
||||
item["label"] = std::to_string(statistics::sum_str_int_map(value));
|
||||
item["label"] = std::to_string(statistics_t::sum_str_int_map(value));
|
||||
data.emplace("stat_detail", item);
|
||||
|
||||
item["label"] = has_cost ? std::to_string(statistics::sum_cost_str_int_map(value)) : font::unicode_em_dash;
|
||||
item["label"] = has_cost ? std::to_string(statistics_t::sum_cost_str_int_map(value)) : font::unicode_em_dash;
|
||||
data.emplace("stat_cost", item);
|
||||
|
||||
stat_list.add_row(data);
|
||||
|
@ -158,7 +158,7 @@ void statistics_dialog::add_damage_row(
|
|||
item["label"] = type;
|
||||
data.emplace("damage_type", item);
|
||||
|
||||
static const int shift = statistics::stats::decimal_shift;
|
||||
static const int shift = statistics_t::stats::decimal_shift;
|
||||
|
||||
const auto damage_str = [](long long damage, long long expected) {
|
||||
const long long shifted = ((expected * 20) + shift) / (2 * shift);
|
||||
|
@ -203,7 +203,7 @@ struct hitrate_table_element
|
|||
};
|
||||
|
||||
// Return the strings to use in the "Hits" table, showing actual and expected number of hits.
|
||||
static hitrate_table_element tally(const statistics::stats::hitrate_map& by_cth, const bool more_is_better)
|
||||
static hitrate_table_element tally(const statistics_t::stats::hitrate_map& by_cth, const bool more_is_better)
|
||||
{
|
||||
unsigned int overall_hits = 0;
|
||||
double expected_hits = 0;
|
||||
|
@ -306,8 +306,8 @@ static hitrate_table_element tally(const statistics::stats::hitrate_map& by_cth,
|
|||
void statistics_dialog::add_hits_row(
|
||||
const std::string& type,
|
||||
const bool more_is_better,
|
||||
const statistics::stats::hitrate_map& by_cth,
|
||||
const statistics::stats::hitrate_map& turn_by_cth,
|
||||
const statistics_t::stats::hitrate_map& by_cth,
|
||||
const statistics_t::stats::hitrate_map& turn_by_cth,
|
||||
const bool show_this_turn)
|
||||
{
|
||||
listbox& hits_list = find_widget<listbox>(get_window(), "stats_list_hits", false);
|
||||
|
@ -363,7 +363,7 @@ void statistics_dialog::update_lists()
|
|||
stat_list.clear();
|
||||
main_stat_table_.clear();
|
||||
|
||||
const statistics::stats& stats = current_stats();
|
||||
const statistics_t::stats& stats = current_stats();
|
||||
|
||||
add_stat_row(_("stats^Recruits"), stats.recruits);
|
||||
add_stat_row(_("Recalls"), stats.recalls);
|
||||
|
|
|
@ -28,7 +28,7 @@ namespace dialogs
|
|||
class statistics_dialog : public modal_dialog
|
||||
{
|
||||
public:
|
||||
statistics_dialog(const team& current_team);
|
||||
statistics_dialog(statistics_t& statistics, const team& current_team);
|
||||
|
||||
DEFINE_SIMPLE_DISPLAY_WRAPPER(statistics_dialog)
|
||||
|
||||
|
@ -40,9 +40,9 @@ private:
|
|||
/**
|
||||
* Picks out the stats structure that was selected for displaying.
|
||||
*/
|
||||
inline const statistics::stats& current_stats();
|
||||
inline const statistics_t::stats& current_stats();
|
||||
|
||||
void add_stat_row(const std::string& type, const statistics::stats::str_int_map& value, const bool has_cost = true);
|
||||
void add_stat_row(const std::string& type, const statistics_t::stats::str_int_map& value, const bool has_cost = true);
|
||||
|
||||
/** Add a row to the Damage table */
|
||||
void add_damage_row(
|
||||
|
@ -64,8 +64,8 @@ private:
|
|||
void add_hits_row(
|
||||
const std::string& type,
|
||||
const bool more_is_better,
|
||||
const statistics::stats::hitrate_map& by_cth,
|
||||
const statistics::stats::hitrate_map& turn_by_cth,
|
||||
const statistics_t::stats::hitrate_map& by_cth,
|
||||
const statistics_t::stats::hitrate_map& turn_by_cth,
|
||||
const bool show_this_turn);
|
||||
|
||||
void update_lists();
|
||||
|
@ -75,12 +75,12 @@ private:
|
|||
|
||||
const team& current_team_;
|
||||
|
||||
const statistics::stats campaign_;
|
||||
const statistics::levels scenarios_;
|
||||
const statistics_t::stats campaign_;
|
||||
const statistics_t::levels scenarios_;
|
||||
|
||||
std::size_t selection_index_;
|
||||
|
||||
std::vector<const statistics::stats::str_int_map*> main_stat_table_;
|
||||
std::vector<const statistics_t::stats::str_int_map*> main_stat_table_;
|
||||
};
|
||||
|
||||
} // namespace dialogs
|
||||
|
|
|
@ -139,7 +139,7 @@ void menu_handler::objectives()
|
|||
|
||||
void menu_handler::show_statistics(int side_num)
|
||||
{
|
||||
gui2::dialogs::statistics_dialog::display(board().get_team(side_num));
|
||||
gui2::dialogs::statistics_dialog::display(pc_.statistics(), board().get_team(side_num));
|
||||
}
|
||||
|
||||
void menu_handler::unit_list()
|
||||
|
|
|
@ -139,7 +139,6 @@ private:
|
|||
friend class console_handler;
|
||||
|
||||
// void do_speak(const std::string& message, bool allies_only);
|
||||
// std::vector<std::string> create_unit_table(const statistics::stats::str_int_map& m,unsigned int team);
|
||||
bool has_friends() const;
|
||||
|
||||
game_display* gui_;
|
||||
|
|
|
@ -158,7 +158,7 @@ play_controller::play_controller(const config& level, saved_game& state_of_game,
|
|||
, persist_()
|
||||
, gui_()
|
||||
, xp_mod_(new unit_experience_accelerator(level["experience_modifier"].to_int(100)))
|
||||
, statistics_context_(new statistics::scenario_context(level["name"]))
|
||||
, statistics_context_(new statistics_t(state_of_game.statistics()))
|
||||
, replay_(new replay(state_of_game.get_replay()))
|
||||
, skip_replay_(skip_replay)
|
||||
, skip_story_(state_of_game.skip_story())
|
||||
|
@ -301,6 +301,8 @@ void play_controller::init(const config& level)
|
|||
|
||||
void play_controller::reset_gamestate(const config& level, int replay_pos)
|
||||
{
|
||||
// TODO: should we update we update this->level_ with level ?
|
||||
|
||||
resources::gameboard = nullptr;
|
||||
resources::gamedata = nullptr;
|
||||
resources::tod_manager = nullptr;
|
||||
|
@ -1283,7 +1285,10 @@ void play_controller::play_side()
|
|||
// This flag can be set by derived classes (in overridden functions).
|
||||
player_type_changed_ = false;
|
||||
|
||||
statistics::reset_turn_stats(gamestate().board_.get_team(current_side()).save_id_or_number());
|
||||
//TODO: this resets the "current turn" statistics whenever the controller changes,
|
||||
// in particular whenever a game is reloaded, wouldn't it be better if this was
|
||||
// only done, when a sides turn actually ends?
|
||||
statistics().reset_turn_stats(gamestate().board_.get_team(current_side()).save_id_or_number());
|
||||
|
||||
play_side_impl();
|
||||
|
||||
|
|
|
@ -58,9 +58,7 @@ namespace soundsource {
|
|||
class manager;
|
||||
} // namespace soundsource
|
||||
|
||||
namespace statistics {
|
||||
struct scenario_context;
|
||||
} // namespace statistics
|
||||
class statistics_t;
|
||||
|
||||
namespace pathfind {
|
||||
class manager;
|
||||
|
@ -324,6 +322,7 @@ public:
|
|||
|
||||
saved_game& get_saved_game() { return saved_game_; }
|
||||
|
||||
statistics_t& statistics() { return *statistics_context_; }
|
||||
bool is_during_turn() const;
|
||||
bool is_linger_mode() const;
|
||||
|
||||
|
@ -386,7 +385,7 @@ protected:
|
|||
//other objects
|
||||
std::unique_ptr<game_display> gui_;
|
||||
const std::unique_ptr<unit_experience_accelerator> xp_mod_;
|
||||
const std::unique_ptr<const statistics::scenario_context> statistics_context_;
|
||||
const std::unique_ptr<statistics_t> statistics_context_;
|
||||
actions::undo_list& undo_stack() { return *gamestate().undo_stack_; }
|
||||
const actions::undo_list& undo_stack() const { return *gamestate().undo_stack_; }
|
||||
std::unique_ptr<replay> replay_;
|
||||
|
|
|
@ -309,12 +309,12 @@ void playsingle_controller::play_scenario_main_loop()
|
|||
local_players[i] = get_teams()[i].is_local();
|
||||
}
|
||||
|
||||
if(ex.start_replay) {
|
||||
// MP "Back to turn"
|
||||
statistics::read_stats(*ex.stats_);
|
||||
if(ex.stats_) {
|
||||
// "Back to turn"
|
||||
get_saved_game().statistics().read(*ex.stats_);
|
||||
} else {
|
||||
// SP replay
|
||||
statistics::reset_current_scenario();
|
||||
// "Reset Replay To start"
|
||||
get_saved_game().statistics().clear_current_scenario();
|
||||
}
|
||||
|
||||
reset_gamestate(*ex.level, (*ex.level)["replay_pos"]);
|
||||
|
|
|
@ -34,7 +34,6 @@
|
|||
#include "play_controller.hpp"
|
||||
#include "synced_context.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "statistics.hpp"
|
||||
#include "units/unit.hpp"
|
||||
#include "whiteboard/manager.hpp"
|
||||
#include "replay_recorder_base.hpp"
|
||||
|
|
|
@ -128,6 +128,7 @@ saved_game::saved_game()
|
|||
, starting_point_type_(starting_point::NONE)
|
||||
, starting_point_()
|
||||
, replay_data_()
|
||||
, statistics_()
|
||||
, skip_story_(false)
|
||||
{
|
||||
}
|
||||
|
@ -141,6 +142,7 @@ saved_game::saved_game(config cfg)
|
|||
, starting_point_type_(starting_point::NONE)
|
||||
, starting_point_()
|
||||
, replay_data_()
|
||||
, statistics_()
|
||||
, skip_story_(false)
|
||||
{
|
||||
set_data(cfg);
|
||||
|
@ -155,6 +157,7 @@ saved_game::saved_game(const saved_game& state)
|
|||
, starting_point_type_(state.starting_point_type_)
|
||||
, starting_point_(state.starting_point_)
|
||||
, replay_data_(state.replay_data_)
|
||||
, statistics_(state.statistics_)
|
||||
, skip_story_(state.skip_story_)
|
||||
{
|
||||
}
|
||||
|
@ -188,7 +191,6 @@ void saved_game::write_config(config_writer& out) const
|
|||
|
||||
out.open_child("replay");
|
||||
replay_data_.write(out);
|
||||
|
||||
out.close_child("replay");
|
||||
write_carryover(out);
|
||||
}
|
||||
|
@ -212,6 +214,9 @@ void saved_game::write_general_info(config_writer& out) const
|
|||
{
|
||||
out.write(classification_.to_config());
|
||||
out.write_child("multiplayer", mp_settings_.to_config());
|
||||
out.open_child("statistics");
|
||||
statistics().write(out);
|
||||
out.close_child("statistics");
|
||||
}
|
||||
|
||||
void saved_game::set_defaults()
|
||||
|
@ -555,6 +560,7 @@ void saved_game::expand_carryover()
|
|||
}
|
||||
|
||||
carryover_ = sides.to_config();
|
||||
statistics().new_scenario(get_starting_point()["name"]);
|
||||
has_carryover_expanded_ = true;
|
||||
}
|
||||
}
|
||||
|
@ -650,6 +656,7 @@ config saved_game::to_config() const
|
|||
|
||||
r.add_child(has_carryover_expanded_ ? "carryover_sides" : "carryover_sides_start", carryover_);
|
||||
r.add_child("multiplayer", mp_settings_.to_config());
|
||||
r.add_child("statistics", statistics_.to_config());
|
||||
|
||||
return r;
|
||||
}
|
||||
|
@ -780,8 +787,7 @@ void saved_game::set_data(config& cfg)
|
|||
LOG_NG << "scenario: '" << carryover_["next_scenario"].str() << "'";
|
||||
|
||||
if(auto stats = cfg.optional_child("statistics")) {
|
||||
statistics::fresh_stats();
|
||||
statistics::read_stats(*stats);
|
||||
statistics_.read(*stats);
|
||||
}
|
||||
|
||||
classification_ = game_classification{ cfg };
|
||||
|
@ -800,6 +806,7 @@ void saved_game::clear()
|
|||
replay_start_.clear();
|
||||
starting_point_.clear();
|
||||
starting_point_type_ = starting_point::NONE;
|
||||
statistics_ = statistics_record::campaign_stats_t();
|
||||
}
|
||||
|
||||
void swap(saved_game& lhs, saved_game& rhs)
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
#include "game_classification.hpp"
|
||||
#include "mp_game_settings.hpp"
|
||||
#include "replay_recorder_base.hpp"
|
||||
#include "statistics_record.hpp"
|
||||
|
||||
class config_writer;
|
||||
|
||||
|
@ -106,6 +107,10 @@ public:
|
|||
{
|
||||
return starting_point_type_ == starting_point::SNAPSHOT;
|
||||
}
|
||||
bool is_start_of_scenario() const
|
||||
{
|
||||
return !has_carryover_expanded_;
|
||||
}
|
||||
/**
|
||||
* converts a normal savegame form the end of a scenaio to a start-of-scenario savefile for the next scenaio,
|
||||
* The saved_game must contain a [snapshot] made during the linger mode of the last scenaio.
|
||||
|
@ -135,6 +140,8 @@ public:
|
|||
replay_recorder_base& get_replay() { return replay_data_; }
|
||||
const replay_recorder_base& get_replay() const { return replay_data_; }
|
||||
|
||||
statistics_record::campaign_stats_t& statistics() { return statistics_; }
|
||||
const statistics_record::campaign_stats_t& statistics() const { return statistics_; }
|
||||
/** Whether to play [story] tags */
|
||||
bool skip_story() const { return skip_story_; }
|
||||
void set_skip_story(bool skip_story) { skip_story_ = skip_story; }
|
||||
|
@ -162,6 +169,8 @@ private:
|
|||
|
||||
replay_recorder_base replay_data_;
|
||||
|
||||
statistics_record::campaign_stats_t statistics_;
|
||||
|
||||
bool skip_story_;
|
||||
};
|
||||
|
||||
|
|
|
@ -44,7 +44,6 @@
|
|||
#include "serialization/binary_or_text.hpp"
|
||||
#include "serialization/parser.hpp"
|
||||
#include "serialization/utf8_exception.hpp"
|
||||
#include "statistics.hpp"
|
||||
#include "video.hpp" // only for faked
|
||||
|
||||
#include <algorithm>
|
||||
|
@ -530,9 +529,6 @@ void savegame::write_game(config_writer& out)
|
|||
out.write_key_val("version", game_config::wesnoth_version.str());
|
||||
|
||||
gamestate_.write_general_info(out);
|
||||
out.open_child("statistics");
|
||||
statistics::write_stats(out);
|
||||
out.close_child("statistics");
|
||||
}
|
||||
|
||||
void savegame::finish_save_game(const config_writer& out)
|
||||
|
|
|
@ -18,14 +18,13 @@
|
|||
* Manage statistics: recruitments, recalls, kills, losses, etc.
|
||||
*/
|
||||
|
||||
#include "game_board.hpp"
|
||||
#include "statistics.hpp"
|
||||
#include "game_board.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"
|
||||
#include "units/types.hpp"
|
||||
#include "units/unit.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
|
@ -33,510 +32,38 @@ static lg::log_domain log_engine("engine");
|
|||
#define DBG_NG LOG_STREAM(debug, log_engine)
|
||||
#define ERR_NG LOG_STREAM(err, log_engine)
|
||||
|
||||
namespace {
|
||||
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)
|
||||
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
|
||||
statistics_t::statistics_t(statistics_record::campaign_stats_t& record)
|
||||
: record_(record)
|
||||
{
|
||||
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::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";
|
||||
}
|
||||
}
|
||||
|
||||
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.mandatory_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";
|
||||
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 auto c = cfg.optional_child("recruits")) {
|
||||
recruits = read_str_int_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("recalls")) {
|
||||
recalls = read_str_int_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("advances")) {
|
||||
advanced_to = read_str_int_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("deaths")) {
|
||||
deaths = read_str_int_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("killed")) {
|
||||
killed = read_str_int_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("recalls")) {
|
||||
recalls = read_str_int_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("attacks")) {
|
||||
attacks_inflicted = read_battle_result_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("defends")) {
|
||||
defends_inflicted = read_battle_result_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("attacks_taken")) {
|
||||
attacks_taken = read_battle_result_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("defends_taken")) {
|
||||
defends_taken = read_battle_result_map(c.value());
|
||||
}
|
||||
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 auto c = cfg.optional_child("turn_by_cth_inflicted")) {
|
||||
turn_by_cth_inflicted = read_by_cth_map(c.value());
|
||||
}
|
||||
if (const auto c = cfg.optional_child("turn_by_cth_taken")) {
|
||||
turn_by_cth_taken = read_by_cth_map(c.value());
|
||||
}
|
||||
|
||||
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()
|
||||
statistics_attack_context::statistics_attack_context(
|
||||
statistics_t& stats, const unit& a, const unit& d, int a_cth, int d_cth)
|
||||
: stats_(&stats)
|
||||
, 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()
|
||||
statistics_attack_context::~statistics_attack_context()
|
||||
{
|
||||
std::string attacker_key = "s" + attacker_res;
|
||||
std::string defender_key = "s" + defender_res;
|
||||
|
@ -548,33 +75,32 @@ attack_context::~attack_context()
|
|||
defender_stats().defends_taken[chance_to_hit_defender][attacker_key]++;
|
||||
}
|
||||
|
||||
stats& attack_context::attacker_stats()
|
||||
statistics_attack_context::stats& statistics_attack_context::attacker_stats()
|
||||
{
|
||||
return get_stats(attacker_side);
|
||||
return stats_->get_stats(attacker_side);
|
||||
}
|
||||
|
||||
stats& attack_context::defender_stats()
|
||||
statistics_attack_context::stats& statistics_attack_context::defender_stats()
|
||||
{
|
||||
return get_stats(defender_side);
|
||||
return stats_->get_stats(defender_side);
|
||||
}
|
||||
|
||||
void attack_context::attack_expected_damage(double attacker_inflict_, double defender_inflict_)
|
||||
void statistics_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;
|
||||
att_stats.expected_damage_taken += defender_inflict;
|
||||
def_stats.expected_damage_inflicted += defender_inflict;
|
||||
def_stats.expected_damage_taken += attacker_inflict;
|
||||
def_stats.expected_damage_taken += attacker_inflict;
|
||||
att_stats.turn_expected_damage_inflicted += attacker_inflict;
|
||||
att_stats.turn_expected_damage_taken += defender_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;
|
||||
def_stats.turn_expected_damage_taken += attacker_inflict;
|
||||
}
|
||||
|
||||
|
||||
void attack_context::attack_result(hit_result res, int cth, int damage, int drain)
|
||||
void statistics_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();
|
||||
|
@ -592,15 +118,15 @@ void attack_context::attack_result(hit_result res, int cth, int damage, int drai
|
|||
|
||||
if(res != MISSES) {
|
||||
// handle drain
|
||||
att_stats.damage_taken -= drain;
|
||||
def_stats.damage_inflicted -= drain;
|
||||
att_stats.turn_damage_taken -= 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.damage_inflicted += damage;
|
||||
def_stats.damage_taken += damage;
|
||||
att_stats.turn_damage_inflicted += damage;
|
||||
def_stats.turn_damage_taken += damage;
|
||||
def_stats.turn_damage_taken += damage;
|
||||
}
|
||||
|
||||
if(res == KILLS) {
|
||||
|
@ -609,7 +135,7 @@ void attack_context::attack_result(hit_result res, int cth, int damage, int drai
|
|||
}
|
||||
}
|
||||
|
||||
void attack_context::defend_result(hit_result res, int cth, int damage, int drain)
|
||||
void statistics_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();
|
||||
|
@ -644,49 +170,43 @@ void attack_context::defend_result(hit_result res, int cth, int damage, int drai
|
|||
}
|
||||
}
|
||||
|
||||
void recruit_unit(const unit& u)
|
||||
void statistics_t::recruit_unit(const unit& u)
|
||||
{
|
||||
stats& s = get_stats(get_team_save_id(u));
|
||||
s.recruits[u.type().parent_id()]++;
|
||||
s.recruit_cost += u.cost();
|
||||
}
|
||||
|
||||
void recall_unit(const unit& u)
|
||||
void statistics_t::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)
|
||||
void statistics_t::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)
|
||||
void statistics_t::un_recruit_unit(const unit& u)
|
||||
{
|
||||
stats& s = get_stats(get_team_save_id(u));
|
||||
s.recruits[u.type().parent_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)
|
||||
void statistics_t::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)
|
||||
void statistics_t::reset_turn_stats(const std::string& save_id)
|
||||
{
|
||||
stats &s = get_stats(save_id);
|
||||
stats& s = get_stats(save_id);
|
||||
s.turn_damage_inflicted = 0;
|
||||
s.turn_damage_taken = 0;
|
||||
s.turn_expected_damage_inflicted = 0;
|
||||
|
@ -696,23 +216,23 @@ void reset_turn_stats(const std::string & save_id)
|
|||
s.save_id = save_id;
|
||||
}
|
||||
|
||||
stats calculate_stats(const std::string & save_id)
|
||||
statistics_t::stats statistics_t::calculate_stats(const std::string& save_id)
|
||||
{
|
||||
stats res;
|
||||
|
||||
DBG_NG << "calculate_stats, side: " << save_id << " master_stats.size: " << master_stats.size();
|
||||
DBG_NG << "calculate_stats, side: " << save_id << " master_stats.size: " << master_stats().size();
|
||||
// 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);
|
||||
for(std::size_t i = 0; i != master_stats().size(); ++i) {
|
||||
auto find_it = master_stats()[i].team_stats.find(save_id);
|
||||
if(find_it != master_stats()[i].team_stats.end()) {
|
||||
res.merge_with(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
|
||||
|
@ -723,122 +243,61 @@ stats calculate_stats(const std::string & save_id)
|
|||
* 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)
|
||||
statistics_t::levels statistics_t::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;
|
||||
for(std::size_t level = 0; level != master_stats().size(); ++level) {
|
||||
const auto& 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);
|
||||
auto 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);
|
||||
if(level_list.empty()) {
|
||||
level_list.emplace_back(&null_name, &null_stats);
|
||||
}
|
||||
|
||||
return level_list;
|
||||
}
|
||||
|
||||
|
||||
config write_stats()
|
||||
statistics_t::stats& statistics_t::get_stats(const std::string& save_id)
|
||||
{
|
||||
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());
|
||||
if(master_stats().empty()) {
|
||||
master_stats().emplace_back(std::string());
|
||||
}
|
||||
|
||||
return res;
|
||||
return master_stats().back().team_stats[save_id];
|
||||
}
|
||||
|
||||
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 std::map<std::string,int>& m)
|
||||
int statistics_t::sum_str_int_map(const std::map<std::string, int>& m)
|
||||
{
|
||||
int res = 0;
|
||||
for(stats::str_int_map::const_iterator i = m.begin(); i != m.end(); ++i) {
|
||||
res += i->second;
|
||||
for(const auto& pair: m) {
|
||||
res += pair.second;
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
int sum_cost_str_int_map(const std::map<std::string,int>& m)
|
||||
int statistics_t::sum_cost_str_int_map(const std::map<std::string, int>& 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.";
|
||||
for(const auto& pair : m) {
|
||||
const unit_type* t = unit_types.find(pair.first);
|
||||
if(!t) {
|
||||
ERR_NG << "Statistics refer to unknown unit type '" << pair.first << "'. Discarding.";
|
||||
} else {
|
||||
cost += i->second * t->cost();
|
||||
cost += pair.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;
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
|
||||
#pragma once
|
||||
|
||||
#include "statistics_record.hpp"
|
||||
|
||||
class config;
|
||||
class config_writer;
|
||||
class unit;
|
||||
|
@ -22,119 +24,60 @@ class unit;
|
|||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace statistics
|
||||
class statistics_t
|
||||
{
|
||||
struct stats
|
||||
{
|
||||
stats();
|
||||
explicit stats(const config& cfg);
|
||||
public:
|
||||
using stats = statistics_record::stats_t;
|
||||
|
||||
config write() const;
|
||||
void write(config_writer &out) const;
|
||||
void read(const config& cfg);
|
||||
|
||||
typedef std::map<std::string,int> str_int_map;
|
||||
str_int_map recruits, recalls, advanced_to, deaths, killed;
|
||||
int recruit_cost, recall_cost;
|
||||
|
||||
/*
|
||||
* A type that will map a string of hit/miss to the number of times
|
||||
* that sequence has occurred.
|
||||
*/
|
||||
typedef str_int_map battle_sequence_frequency_map;
|
||||
|
||||
/** A type that will map different % chances to hit to different results. */
|
||||
typedef std::map<int,battle_sequence_frequency_map> battle_result_map;
|
||||
|
||||
/** Statistics of this side's attacks on its own turns. */
|
||||
battle_result_map attacks_inflicted;
|
||||
/** Statistics of this side's attacks on enemies' turns. */
|
||||
battle_result_map defends_inflicted;
|
||||
/** Statistics of enemies' counter attacks on this side's turns. */
|
||||
battle_result_map attacks_taken;
|
||||
/** Statistics of enemies' attacks against this side on their turns. */
|
||||
battle_result_map defends_taken;
|
||||
|
||||
long long damage_inflicted, damage_taken;
|
||||
long long turn_damage_inflicted, turn_damage_taken;
|
||||
|
||||
struct hitrate_t
|
||||
{
|
||||
int strikes; //< Number of strike attempts at the given CTH
|
||||
int hits; //< Number of strikes that hit at the given CTH
|
||||
hitrate_t() = default;
|
||||
explicit hitrate_t(const config& cfg);
|
||||
config write() const;
|
||||
};
|
||||
/** A type that maps chance-to-hit percentage to number of hits and strikes at that CTH. */
|
||||
typedef std::map<int, hitrate_t> hitrate_map;
|
||||
hitrate_map by_cth_inflicted, by_cth_taken;
|
||||
hitrate_map turn_by_cth_inflicted, turn_by_cth_taken;
|
||||
|
||||
static const int decimal_shift = 1000;
|
||||
|
||||
// Expected value for damage inflicted/taken * 1000, based on
|
||||
// probability to hit,
|
||||
// Use this long term to see how lucky a side is.
|
||||
|
||||
long long expected_damage_inflicted, expected_damage_taken;
|
||||
long long turn_expected_damage_inflicted, turn_expected_damage_taken;
|
||||
std::string save_id;
|
||||
};
|
||||
|
||||
int sum_str_int_map(const std::map<std::string,int>& m);
|
||||
int sum_cost_str_int_map(const std::map<std::string,int>& m);
|
||||
|
||||
struct scenario_context
|
||||
{
|
||||
scenario_context(const std::string& name);
|
||||
~scenario_context();
|
||||
};
|
||||
|
||||
struct attack_context
|
||||
{
|
||||
attack_context(const unit& a, const unit& d, int a_cth, int d_cth);
|
||||
~attack_context();
|
||||
|
||||
enum hit_result { MISSES, HITS, KILLS };
|
||||
|
||||
void attack_expected_damage(double attacker_inflict, double defender_inflict);
|
||||
void attack_result(hit_result res, int cth, int damage, int drain);
|
||||
void defend_result(hit_result res, int cth, int damage, int drain);
|
||||
|
||||
private:
|
||||
|
||||
std::string attacker_type, defender_type;
|
||||
std::string attacker_side, defender_side;
|
||||
int chance_to_hit_defender, chance_to_hit_attacker;
|
||||
std::string attacker_res, defender_res;
|
||||
|
||||
stats& attacker_stats();
|
||||
stats& defender_stats();
|
||||
};
|
||||
statistics_t(statistics_record::campaign_stats_t& record);
|
||||
|
||||
void recruit_unit(const unit& u);
|
||||
void recall_unit(const unit& u);
|
||||
void un_recall_unit(const unit& u);
|
||||
void un_recruit_unit(const unit& u);
|
||||
int un_recall_unit_cost(const unit& u);
|
||||
|
||||
void advance_unit(const unit& u);
|
||||
|
||||
config write_stats();
|
||||
void write_stats(config_writer &out);
|
||||
void read_stats(const config& cfg);
|
||||
void fresh_stats();
|
||||
/** Delete the current scenario from the stats. */
|
||||
void clear_current_scenario();
|
||||
/** Reset the stats of the current scenario to the beginning. */
|
||||
void reset_current_scenario();
|
||||
|
||||
void reset_turn_stats(const std::string & save_id);
|
||||
stats calculate_stats(const std::string & save_id);
|
||||
/** Stats (and name) for each scenario. The pointers are never nullptr. */
|
||||
typedef std::vector< std::pair<const std::string *, const stats *>> levels;
|
||||
/** Returns a list of names and stats for each scenario in the current campaign. */
|
||||
levels level_stats(const std::string & save_id);
|
||||
} // end namespace statistics
|
||||
std::ostream& operator<<(std::ostream& outstream, const statistics::stats::hitrate_t& by_cth);
|
||||
/// returns the stats for the given side in the current scenario.
|
||||
stats& get_stats(const std::string &save_id);
|
||||
|
||||
static int sum_str_int_map(const std::map<std::string,int>& m);
|
||||
static int sum_cost_str_int_map(const std::map<std::string,int>& m);
|
||||
private:
|
||||
statistics_record::campaign_stats_t& record_;
|
||||
|
||||
auto& master_stats() {
|
||||
return record_.master_record;
|
||||
}
|
||||
};
|
||||
|
||||
struct statistics_attack_context
|
||||
{
|
||||
using stats = statistics_t::stats;
|
||||
|
||||
statistics_attack_context(statistics_t& stats, const unit& a, const unit& d, int a_cth, int d_cth);
|
||||
~statistics_attack_context();
|
||||
enum hit_result { MISSES, HITS, KILLS };
|
||||
|
||||
void attack_expected_damage(double attacker_inflict, double defender_inflict);
|
||||
void attack_result(hit_result res, int cth, int damage, int drain);
|
||||
void defend_result(hit_result res, int cth, int damage, int drain);
|
||||
private:
|
||||
|
||||
/// never nullptr
|
||||
statistics_t* stats_;
|
||||
|
||||
std::string attacker_type, defender_type;
|
||||
std::string attacker_side, defender_side;
|
||||
int chance_to_hit_defender, chance_to_hit_attacker;
|
||||
std::string attacker_res, defender_res;
|
||||
|
||||
stats& attacker_stats();
|
||||
stats& defender_stats();
|
||||
};
|
||||
|
|
515
src/statistics_record.cpp
Normal file
515
src/statistics_record.cpp
Normal file
|
@ -0,0 +1,515 @@
|
|||
/*
|
||||
Copyright (C) 2023
|
||||
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: saving and reading data.
|
||||
*/
|
||||
|
||||
#include "statistics_record.hpp"
|
||||
#include "log.hpp"
|
||||
#include "serialization/binary_or_text.hpp"
|
||||
#include "serialization/string_utils.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
static lg::log_domain log_engine("engine");
|
||||
#define DBG_NG LOG_STREAM(debug, log_engine)
|
||||
#define ERR_NG LOG_STREAM(err, log_engine)
|
||||
|
||||
namespace statistics_record
|
||||
{
|
||||
|
||||
static config write_str_int_map(const stats_t::str_int_map& m)
|
||||
{
|
||||
config res;
|
||||
for(stats_t::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_t::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_t::str_int_map::value_type p) { return std::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_t::str_int_map read_str_int_map(const config& cfg)
|
||||
{
|
||||
stats_t::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";
|
||||
}
|
||||
}
|
||||
|
||||
return m;
|
||||
}
|
||||
|
||||
static config write_battle_result_map(const stats_t::battle_result_map& m)
|
||||
{
|
||||
config res;
|
||||
for(stats_t::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_t::battle_result_map& m)
|
||||
{
|
||||
for(stats_t::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_t::battle_result_map read_battle_result_map(const config& cfg)
|
||||
{
|
||||
stats_t::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_t::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_t::battle_result_map& a, const stats_t::battle_result_map& b);
|
||||
|
||||
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)
|
||||
{
|
||||
stats_t::hitrate_map m;
|
||||
|
||||
stats_t::battle_result_map merged = attacks;
|
||||
merge_battle_result_maps(merged, defends);
|
||||
|
||||
for(const auto& i : merged) {
|
||||
int cth = i.first;
|
||||
const stats_t::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_t::hitrate_map read_by_cth_map(const config& cfg)
|
||||
{
|
||||
stats_t::hitrate_map m;
|
||||
for(const config& i : cfg.child_range("hitrate_map_entry")) {
|
||||
m.emplace(i["cth"], stats_t::hitrate_t(i.mandatory_child("stats")));
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
static void merge_str_int_map(stats_t::str_int_map& a, const stats_t::str_int_map& b)
|
||||
{
|
||||
for(stats_t::str_int_map::const_iterator i = b.begin(); i != b.end(); ++i) {
|
||||
a[i->first] += i->second;
|
||||
}
|
||||
}
|
||||
|
||||
static void merge_battle_result_maps(stats_t::battle_result_map& a, const stats_t::battle_result_map& b)
|
||||
{
|
||||
for(stats_t::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_t::hitrate_map& a, const stats_t::hitrate_map& b)
|
||||
{
|
||||
for(const auto& i : b) {
|
||||
a[i.first].hits += i.second.hits;
|
||||
a[i.first].strikes += i.second.strikes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stats_t::stats_t()
|
||||
: 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_t::stats_t(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_t::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_t::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_t::read(const config& cfg)
|
||||
{
|
||||
if(const auto c = cfg.optional_child("recruits")) {
|
||||
recruits = read_str_int_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("recalls")) {
|
||||
recalls = read_str_int_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("advances")) {
|
||||
advanced_to = read_str_int_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("deaths")) {
|
||||
deaths = read_str_int_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("killed")) {
|
||||
killed = read_str_int_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("recalls")) {
|
||||
recalls = read_str_int_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("attacks")) {
|
||||
attacks_inflicted = read_battle_result_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("defends")) {
|
||||
defends_inflicted = read_battle_result_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("attacks_taken")) {
|
||||
attacks_taken = read_battle_result_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("defends_taken")) {
|
||||
defends_taken = read_battle_result_map(c.value());
|
||||
}
|
||||
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 auto c = cfg.optional_child("turn_by_cth_inflicted")) {
|
||||
turn_by_cth_inflicted = read_by_cth_map(c.value());
|
||||
}
|
||||
if(const auto c = cfg.optional_child("turn_by_cth_taken")) {
|
||||
turn_by_cth_taken = read_by_cth_map(c.value());
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
void stats_t::merge_with(const stats_t& b)
|
||||
{
|
||||
stats_t& a = *this;
|
||||
DBG_NG << "Merging statistics";
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
scenario_stats_t::scenario_stats_t(const config& cfg)
|
||||
: team_stats()
|
||||
, scenario_name(cfg["scenario"])
|
||||
{
|
||||
for(const config& team : cfg.child_range("team")) {
|
||||
team_stats[team["save_id"]] = stats_t(team);
|
||||
}
|
||||
}
|
||||
|
||||
config scenario_stats_t::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_t::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");
|
||||
}
|
||||
}
|
||||
|
||||
config stats_t::hitrate_t::write() const
|
||||
{
|
||||
return config("hits", hits, "strikes", strikes);
|
||||
}
|
||||
|
||||
stats_t::hitrate_t::hitrate_t(const config& cfg)
|
||||
: strikes(cfg["strikes"])
|
||||
, hits(cfg["hits"])
|
||||
{
|
||||
}
|
||||
|
||||
config campaign_stats_t::to_config() const
|
||||
{
|
||||
config res;
|
||||
|
||||
for(std::vector<scenario_stats_t>::const_iterator i = master_record.begin(); i != master_record.end(); ++i) {
|
||||
res.add_child("scenario", i->write());
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
void campaign_stats_t::write(config_writer& out) const
|
||||
{
|
||||
for(std::vector<scenario_stats_t>::const_iterator i = master_record.begin(); i != master_record.end(); ++i) {
|
||||
out.open_child("scenario");
|
||||
i->write(out);
|
||||
out.close_child("scenario");
|
||||
}
|
||||
}
|
||||
|
||||
void campaign_stats_t::read(const config& cfg, bool append)
|
||||
{
|
||||
if(!append) {
|
||||
master_record.clear();
|
||||
}
|
||||
for(const config& s : cfg.child_range("scenario")) {
|
||||
master_record.emplace_back(s);
|
||||
}
|
||||
}
|
||||
|
||||
void campaign_stats_t::new_scenario(const std::string& name)
|
||||
{
|
||||
master_record.emplace_back(name);
|
||||
}
|
||||
|
||||
void campaign_stats_t::clear_current_scenario()
|
||||
{
|
||||
if(master_record.empty() == false) {
|
||||
master_record.back().team_stats.clear();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace statistics_record
|
||||
|
||||
std::ostream& operator<<(std::ostream& outstream, const statistics_record::stats_t::hitrate_t& by_cth)
|
||||
{
|
||||
outstream << "[" << by_cth.hits << "/" << by_cth.strikes << "]";
|
||||
return outstream;
|
||||
}
|
127
src/statistics_record.hpp
Normal file
127
src/statistics_record.hpp
Normal file
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
Copyright (C) 2003 - 2023
|
||||
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.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
class config;
|
||||
class config_writer;
|
||||
|
||||
#include <string>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace statistics_record
|
||||
{
|
||||
struct stats_t
|
||||
{
|
||||
stats_t();
|
||||
explicit stats_t(const config& cfg);
|
||||
|
||||
config write() const;
|
||||
void write(config_writer &out) const;
|
||||
void read(const config& cfg);
|
||||
|
||||
void merge_with(const stats_t& other);
|
||||
|
||||
typedef std::map<std::string,int> str_int_map;
|
||||
str_int_map recruits, recalls, advanced_to, deaths, killed;
|
||||
int recruit_cost, recall_cost;
|
||||
|
||||
/*
|
||||
* A type that will map a string of hit/miss to the number of times
|
||||
* that sequence has occurred.
|
||||
*/
|
||||
typedef str_int_map battle_sequence_frequency_map;
|
||||
|
||||
/** A type that will map different % chances to hit to different results. */
|
||||
typedef std::map<int,battle_sequence_frequency_map> battle_result_map;
|
||||
|
||||
/** Statistics of this side's attacks on its own turns. */
|
||||
battle_result_map attacks_inflicted;
|
||||
/** Statistics of this side's attacks on enemies' turns. */
|
||||
battle_result_map defends_inflicted;
|
||||
/** Statistics of enemies' counter attacks on this side's turns. */
|
||||
battle_result_map attacks_taken;
|
||||
/** Statistics of enemies' attacks against this side on their turns. */
|
||||
battle_result_map defends_taken;
|
||||
|
||||
long long damage_inflicted, damage_taken;
|
||||
long long turn_damage_inflicted, turn_damage_taken;
|
||||
|
||||
struct hitrate_t
|
||||
{
|
||||
int strikes; //< Number of strike attempts at the given CTH
|
||||
int hits; //< Number of strikes that hit at the given CTH
|
||||
hitrate_t() = default;
|
||||
explicit hitrate_t(const config& cfg);
|
||||
config write() const;
|
||||
};
|
||||
/** A type that maps chance-to-hit percentage to number of hits and strikes at that CTH. */
|
||||
typedef std::map<int, hitrate_t> hitrate_map;
|
||||
hitrate_map by_cth_inflicted, by_cth_taken;
|
||||
hitrate_map turn_by_cth_inflicted, turn_by_cth_taken;
|
||||
|
||||
static const int decimal_shift = 1000;
|
||||
|
||||
// Expected value for damage inflicted/taken * 1000, based on
|
||||
// probability to hit,
|
||||
// Use this long term to see how lucky a side is.
|
||||
|
||||
long long expected_damage_inflicted, expected_damage_taken;
|
||||
long long turn_expected_damage_inflicted, turn_expected_damage_taken;
|
||||
std::string save_id;
|
||||
};
|
||||
|
||||
|
||||
using team_stats_t = std::map<std::string, stats_t>;
|
||||
|
||||
struct scenario_stats_t
|
||||
{
|
||||
explicit scenario_stats_t(const std::string& name) :
|
||||
team_stats(),
|
||||
scenario_name(name)
|
||||
{}
|
||||
|
||||
explicit scenario_stats_t(const config& cfg);
|
||||
|
||||
config write() const;
|
||||
void write(config_writer &out) const;
|
||||
|
||||
team_stats_t team_stats;
|
||||
std::string scenario_name;
|
||||
};
|
||||
|
||||
struct campaign_stats_t
|
||||
{
|
||||
campaign_stats_t() = default;
|
||||
explicit campaign_stats_t(const config& cfg)
|
||||
: master_record()
|
||||
{
|
||||
read(cfg);
|
||||
}
|
||||
config to_config() const;
|
||||
void write(config_writer &out) const;
|
||||
void read(const config& cfg, bool append = false);
|
||||
/** Adds an entry for anew scenario to wrte to. */
|
||||
void new_scenario(const std::string & scenario_name);
|
||||
/** Delete the current scenario from the stats. */
|
||||
void clear_current_scenario();
|
||||
|
||||
std::vector<scenario_stats_t> master_record;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& outstream, const statistics_record::stats_t::hitrate_t& by_cth);
|
|
@ -1318,10 +1318,12 @@ template<>
|
|||
struct dialog_tester<statistics_dialog>
|
||||
{
|
||||
team t;
|
||||
dialog_tester() : t() {}
|
||||
statistics_record::campaign_stats_t stats_record;
|
||||
statistics_t stats;
|
||||
dialog_tester() : t() , stats_record(), stats(stats_record) {}
|
||||
statistics_dialog* create()
|
||||
{
|
||||
return new statistics_dialog(t);
|
||||
return new statistics_dialog(stats, t);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
#include "serialization/unicode_cast.hpp"
|
||||
#include "serialization/schema_validator.hpp" // for strict_validation_enabled and schema_validator
|
||||
#include "sound.hpp" // for commit_music_changes, etc
|
||||
#include "statistics.hpp" // for fresh_stats
|
||||
#include "formula/string_utils.hpp" // VGETTEXT
|
||||
#include <functional>
|
||||
#include "game_version.hpp" // for version_info
|
||||
|
@ -868,8 +867,6 @@ static int do_gameloop(const std::vector<std::string>& args)
|
|||
plugins.set_callback("exit", [](const config& cfg) { safe_exit(cfg["code"].to_int(0)); }, false);
|
||||
|
||||
while(true) {
|
||||
statistics::fresh_stats();
|
||||
|
||||
if(!game->has_load_data()) {
|
||||
auto cfg = config_manager.game_config().optional_child("titlescreen_music");
|
||||
if(cfg) {
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
#include "fake_unit_manager.hpp"
|
||||
#include "fake_unit_ptr.hpp"
|
||||
#include "game_board.hpp"
|
||||
#include "play_controller.hpp"
|
||||
#include "recall_list_manager.hpp"
|
||||
#include "resources.hpp"
|
||||
#include "replay_helper.hpp"
|
||||
|
@ -201,7 +202,7 @@ void recall::draw_hex(const map_location& hex)
|
|||
//position 0,0 in the hex is the upper left corner
|
||||
std::stringstream number_text;
|
||||
unit &it = *get_unit();
|
||||
int cost = statistics::un_recall_unit_cost(it);
|
||||
int cost = it.recall_cost();
|
||||
if (cost < 0) {
|
||||
number_text << font::unicode_minus << resources::gameboard->teams().at(team_index()).recall_cost();
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue