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:
gfgtdf 2023-05-02 17:11:34 +02:00
parent b567b4286c
commit 01f28b12ae
29 changed files with 849 additions and 790 deletions

View file

@ -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" />

View file

@ -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;
};

View file

@ -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

View file

@ -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.";
}

View file

@ -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;

View file

@ -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.

View file

@ -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());
}

View file

@ -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

View file

@ -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);

View file

@ -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()) {

View file

@ -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_);

View file

@ -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);

View file

@ -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

View file

@ -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()

View file

@ -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_;

View file

@ -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();

View file

@ -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_;

View file

@ -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"]);

View file

@ -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"

View file

@ -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)

View file

@ -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_;
};

View file

@ -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)

View file

@ -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;
}

View file

@ -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
View 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
View 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);

View file

@ -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);
}
};

View file

@ -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) {

View file

@ -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();
}