The Battle for Wesnoth  1.15.1+dev
statistics_dialog.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2016 - 2018 by the Battle for Wesnoth Project https://www.wesnoth.org/
3 
4  This program is free software; you can redistribute it and/or modify
5  it under the terms of the GNU General Public License as published by
6  the Free Software Foundation; either version 2 of the License, or
7  (at your option) any later version.
8  This program is distributed in the hope that it will be useful,
9  but WITHOUT ANY WARRANTY.
10 
11  See the COPYING file for more details.
12 */
13 
14 #define GETTEXT_DOMAIN "wesnoth-lib"
15 
17 
18 #include "actions/attack.hpp" // for battle_context_unit_stats
19 #include "font/constants.hpp"
20 #include "font/text_formatting.hpp"
21 #include "formatter.hpp"
22 #include "formula/string_utils.hpp"
23 #include "gettext.hpp"
25 #include "gui/widgets/label.hpp"
26 #include "gui/widgets/listbox.hpp"
28 #include "gui/widgets/settings.hpp"
29 #include "gui/widgets/window.hpp"
30 #include "team.hpp"
31 #include "units/types.hpp"
32 #include "utils/functional.hpp"
33 
34 #include <iomanip>
35 #include <iostream>
36 #include <memory>
37 
38 // TODO duplicated from attack_predictions.cpp
39 static std::string get_probability_string(const double prob)
40 {
41  std::ostringstream ss;
42 
43  if(prob > 0.9995) {
44  ss << "100";
45  } else {
46  ss << std::fixed << std::setprecision(1) << 100.0 * prob;
47  }
48 
49  return ss.str();
50 }
51 
52 namespace gui2
53 {
54 namespace dialogs
55 {
56 REGISTER_DIALOG(statistics_dialog)
57 
59  : current_team_(current_team)
60  , campaign_(statistics::calculate_stats(current_team.save_id_or_number()))
61  , scenarios_(statistics::level_stats(current_team.save_id_or_number()))
62  , selection_index_(scenarios_.size()) // The extra All Scenarios menu entry makes size() a valid initial index.
63  , main_stat_table_()
64 {
65  set_restore(true);
66 }
67 
69 {
70  //
71  // Set title
72  //
73  label& title = find_widget<label>(&window, "title", false);
74  title.set_label((formatter() << title.get_label() << " (" << current_team_.side_name() << ")").str());
75 
76  //
77  // Set up scenario menu
78  //
79  std::vector<config> menu_items;
80 
81  // Keep this first!
82  menu_items.emplace_back("label", _("All Scenarios"));
83 
84  for(const auto& scenario : scenarios_) {
85  menu_items.emplace_back("label", *scenario.first);
86  }
87 
88  menu_button& scenario_menu = find_widget<menu_button>(&window, "scenario_menu", false);
89 
90  scenario_menu.set_values(menu_items, selection_index_);
91 
92  connect_signal_notify_modified(scenario_menu,
93  std::bind(&statistics_dialog::on_scenario_select, this, std::ref(window)));
94 
95  //
96  // Set up primary stats list
97  //
98  listbox& stat_list = find_widget<listbox>(&window, "stats_list_main", false);
99 
101  std::bind(&statistics_dialog::on_primary_list_select, this, std::ref(window)));
102 
103  update_lists(window);
104 }
105 
107 {
108  return selection_index_ == 0 ? campaign_ : *scenarios_[selection_index_ - 1].second;
109 }
110 
111 void statistics_dialog::add_stat_row(window& window, const std::string& type, const statistics::stats::str_int_map& value, const bool has_cost)
112 {
113  listbox& stat_list = find_widget<listbox>(&window, "stats_list_main", false);
114 
115  std::map<std::string, string_map> data;
117 
118  item["label"] = type;
119  data.emplace("stat_type", item);
120 
121  item["label"] = std::to_string(statistics::sum_str_int_map(value));
122  data.emplace("stat_detail", item);
123 
124  item["label"] = has_cost ? std::to_string(statistics::sum_cost_str_int_map(value)) : font::unicode_em_dash;
125  data.emplace("stat_cost", item);
126 
127  stat_list.add_row(data);
128 
129  main_stat_table_.push_back(&value);
130 }
131 
132 // Generate the string for the "A + B" column of the damage and hits tables.
133 static std::ostream& write_actual_and_expected(std::ostream& str, const long long actual, const double expected)
134 {
135  // This is displayed as a sum or difference, not as "actual/expected", to prevent the string in the next column, str2.str(), from being mistaken for the result of the division.
136  if(expected == 0) {
137  str << "+0% (0 + 0)";
138  } else {
139  str << (formatter() << std::showpos << std::round((actual - expected) * 100 / expected) << "% (").str();
140  str << expected << (actual >= expected ? " + " : " − ")
141  << static_cast<unsigned int>(std::round(std::abs(expected - actual)));
142  str << ')';
143  }
144  return str;
145 }
146 
148  window& window,
149  const std::string& type,
150  const long long& damage,
151  const long long& expected,
152  const long long& turn_damage,
153  const long long& turn_expected,
154  const bool show_this_turn)
155 {
156  listbox& damage_list = find_widget<listbox>(&window, "stats_list_damage", false);
157 
158  std::map<std::string, string_map> data;
160 
161  item["label"] = type;
162  data.emplace("damage_type", item);
163 
164  const int shift = statistics::stats::decimal_shift;
165 
166  const auto damage_str = [shift](long long damage, long long expected) {
167  const long long shifted = ((expected * 20) + shift) / (2 * shift);
168  std::ostringstream str;
169  write_actual_and_expected(str, damage, static_cast<double>(shifted) * 0.1);
170  return str.str();
171  };
172 
173  item["label"] = damage_str(damage, expected);
174  data.emplace("damage_overall", item);
175 
176  item["label"] = "";
177  data.emplace("overall_score", item);
178 
179  if(show_this_turn) {
180  label& this_turn_header = find_widget<label>(&window, "damage_this_turn_header", false);
181  this_turn_header.set_label(_("This Turn"));
182 
183  item["label"] = damage_str(turn_damage, turn_expected);
184  data.emplace("damage_this_turn", item);
185 
186  item["label"] = "";
187  data.emplace("this_turn_score", item);
188  } else {
189  // TODO: Setting the label to "" causes "This Turn" not to be drawn when changing back to the current scenraio view, so set the label to " " (a single space) instead.
190  label& this_turn_header = find_widget<label>(&window, "damage_this_turn_header", false);
191  this_turn_header.set_label(" ");
192  }
193 
194  damage_list.add_row(data);
195 }
196 
197 // Custom type to allow tally() to return two values.
199 {
200  // The string with <actual number of hits>/<expected number of hits>
201  std::string hitrate_str;
202  // The string with the a priori probability of that result
203  std::string pvalue_str;
204  // The tooltip of the table cell - shows the actual (empirical) CTH
205  std::string tooltip;
206 };
207 
208 // Return the strings to use in the "Hits" table, showing actual and expected number of hits.
209 static hitrate_table_element tally(const statistics::stats::hitrate_map& by_cth, const bool more_is_better)
210 {
211  unsigned int overall_hits = 0;
212  double expected_hits = 0;
213  unsigned int overall_strikes = 0;
214 
215  std::ostringstream str, str2, tooltip;
216 
217  tooltip << '\n' << '\n' << _("Actual hit rates, by chance to hit:");
218  if(by_cth.empty())
219  tooltip << '\n' << _("(no attacks have taken place yet)");
220  for(const auto& i : by_cth) {
221  int cth = i.first;
222  overall_hits += i.second.hits;
223  expected_hits += (cth * 0.01) * i.second.strikes;
224  overall_strikes += i.second.strikes;
225  tooltip << "\n" << cth << "%: "
226  << get_probability_string(i.second.hits/static_cast<double>(i.second.strikes))
227  << "% (N=" << i.second.strikes << ")";
228  }
229 
230  write_actual_and_expected(str, overall_hits, expected_hits);
231 
232  // Compute the a priori probability of this actual result, by simulating many attacks against a single defender.
233  {
234  config defender_cfg(
235  "id", "statistics_dialog_dummy_defender",
236  "hide_help", true,
237  "do_not_list", true,
238  "hitpoints", overall_strikes
239  );
240  unit_type defender_type(defender_cfg);
241  unit_types.build_unit_type(defender_type, unit_type::BUILD_STATUS::FULL);
242 
243  battle_context_unit_stats defender_bc(&defender_type, nullptr, false, nullptr, nullptr, 0 /* not used */);
244  std::unique_ptr<combatant> current_defender(new combatant(defender_bc));
245 
246  for(const auto& i : by_cth) {
247  int cth = i.first;
248  config attacker_cfg(
249  "id", "statistics_dialog_dummy_attacker" + std::to_string(cth),
250  "hide_help", true,
251  "do_not_list", true,
252  "hitpoints", 1
253  );
254  unit_type attacker_type(attacker_cfg);
255  unit_types.build_unit_type(attacker_type, unit_type::BUILD_STATUS::FULL);
256 
257  auto attack = std::make_shared<attack_type>(config(
258  "type", "blade",
259  "range", "melee",
260  "name", "dummy attack",
261  "damage", 1,
262  "number", i.second.strikes
263  ));
264 
265  battle_context_unit_stats attacker_bc(&attacker_type, attack, true, &defender_type, nullptr, 100 - cth);
266  defender_bc = battle_context_unit_stats(&defender_type, nullptr, false, &attacker_type, attack, 0 /* not used */);
267 
268  // Update current_defender with the new defender_bc.
269  current_defender.reset(new combatant(*current_defender, defender_bc));
270 
271  combatant attacker(attacker_bc);
272  attacker.fight(*current_defender);
273  }
274 
275  const std::vector<double>& final_hp_dist = current_defender->hp_dist;
276  const auto chance_of_exactly_N_hits = [&final_hp_dist](int n) { return final_hp_dist[final_hp_dist.size() - 1 - n]; };
277 
278  // The a priori probability of scoring less hits than the actual number of hits
279  // aka "percentile" or "p-value"
280  double probability_lt = 0.0;
281  for(unsigned int i = 0; i < overall_hits; ++i) {
282  probability_lt += chance_of_exactly_N_hits(i);
283  }
284  // The a priori probability of scoring exactly the actual number of hits
285  double probability_eq = chance_of_exactly_N_hits(overall_hits);
286  // The a priori probability of scoring more hits than the actual number of hits
287  double probability_gt = 1.0 - (probability_lt + probability_eq);
288 
289  if(overall_strikes == 0) {
290  // Start of turn
291  str2 << font::unicode_em_dash;
292  } else {
293  const auto add_probability = [&str2](double probability, bool more_is_better) {
294  str2 << font::span_color(game_config::red_to_green((more_is_better ? probability : 1.0 - probability) * 100.0, true))
295  << get_probability_string(probability) << "</span>";
296  };
297 
298  // Take the average. At the end of a scenario or a campaign the sum of
299  // probability_lt+probability_gt is very close to 1.0 so the percentile is
300  // approximately equal to probability_lt.
301  const double percentile = (probability_lt + (1.0 - probability_gt)) / 2.0;
302  add_probability(percentile, more_is_better);
303  }
304  }
305 
306  return hitrate_table_element{str.str(), str2.str(), tooltip.str()};
307 }
308 
310  window& window,
311  const std::string& type,
312  const bool more_is_better,
313  const statistics::stats::hitrate_map& by_cth,
314  const statistics::stats::hitrate_map& turn_by_cth,
315  const bool show_this_turn)
316 {
317  listbox& hits_list = find_widget<listbox>(&window, "stats_list_hits", false);
318 
319  std::map<std::string, string_map> data;
321 
322  hitrate_table_element element;
323 
324  item["label"] = type;
325  data.emplace("hits_type", item);
326 
327  const auto tooltip_static_part = _(
328  "stats dialog^Difference of actual outcome to expected outcome, as a percentage.\n"
329  "The first number in parentheses is the expected number of hits inflicted/taken.\n"
330  "The sum (or difference) of the two numbers in parentheses is the actual number of hits inflicted/taken.");
331  element = tally(by_cth, more_is_better);
332  item["tooltip"] = tooltip_static_part + element.tooltip;
333  item["label"] = element.hitrate_str;
334  data.emplace("hits_overall", item);
335 
336  // Don't set the tooltip; it's set in WML.
337  data.emplace("overall_score", string_map { { "label", element.pvalue_str } });
338 
339  if(show_this_turn) {
340  label& this_turn_header = find_widget<label>(&window, "hits_this_turn_header", false);
341  this_turn_header.set_label(_("This Turn"));
342 
343  element = tally(turn_by_cth, more_is_better);
344  item["tooltip"] = tooltip_static_part + element.tooltip;
345  item["label"] = element.hitrate_str;
346  data.emplace("hits_this_turn", item);
347 
348  // Don't set the tooltip; it's set in WML.
349  data.emplace("this_turn_score", string_map { { "label", element.pvalue_str } });
350  } else {
351  // TODO: Setting the label to "" causes "This Turn" not to be drawn when changing back to the current scenraio view, so set the label to " " (a single space) instead.
352  label& this_turn_header = find_widget<label>(&window, "hits_this_turn_header", false);
353  this_turn_header.set_label(" ");
354  }
355 
356  hits_list.add_row(data);
357 }
358 
360 {
361  //
362  // Update primary stats list
363  //
364  listbox& stat_list = find_widget<listbox>(&window, "stats_list_main", false);
365  const int last_selected_stat_row = stat_list.get_selected_row();
366 
367  stat_list.clear();
368  main_stat_table_.clear();
369 
370  const statistics::stats& stats = current_stats();
371 
372  add_stat_row(window, _("Recruits"), stats.recruits);
373  add_stat_row(window, _("Recalls"), stats.recalls);
374  add_stat_row(window, _("Advancements"), stats.advanced_to, false);
375  add_stat_row(window, _("Losses"), stats.deaths);
376  add_stat_row(window, _("Kills"), stats.killed);
377 
378  // Reselect previously selected row. Do this *before* calling on_primary_list_select.
379  if(last_selected_stat_row != -1) {
380  stat_list.select_row(last_selected_stat_row);
381  }
382 
383  // Update unit count list
384  on_primary_list_select(window);
385 
386  //
387  // Update damage stats list
388  //
389  const bool show_this_turn = selection_index_ == scenarios_.size();
390 
391  listbox& damage_list = find_widget<listbox>(&window, "stats_list_damage", false);
392 
393  damage_list.clear();
394 
395  listbox& hits_list = find_widget<listbox>(&window, "stats_list_hits", false);
396  hits_list.clear();
397 
398  add_damage_row(window, _("Inflicted"),
399  stats.damage_inflicted,
401  stats.turn_damage_inflicted,
403  show_this_turn
404  );
405  add_hits_row(window, _("Inflicted"), true,
406  stats.by_cth_inflicted,
407  stats.turn_by_cth_inflicted,
408  show_this_turn
409  );
410 
411  add_damage_row(window, _("Taken"),
412  stats.damage_taken,
413  stats.expected_damage_taken,
414  stats.turn_damage_taken,
416  show_this_turn
417  );
418  add_hits_row(window, _("Taken"), false,
419  stats.by_cth_taken,
420  stats.turn_by_cth_taken,
421  show_this_turn
422  );
423 }
424 
426 {
427  const std::size_t new_index = find_widget<menu_button>(&window, "scenario_menu", false).get_value();
428 
429  if(selection_index_ != new_index) {
430  selection_index_ = new_index;
431  update_lists(window);
432  }
433 }
434 
436 {
437  const int selected_row = find_widget<listbox>(&window, "stats_list_main", false).get_selected_row();
438  if(selected_row == -1) {
439  return;
440  }
441 
442  listbox& unit_list = find_widget<listbox>(&window, "stats_list_units", false);
443 
444  unit_list.clear();
445 
446  for(const auto& i : *main_stat_table_[selected_row]) {
447  const unit_type* type = unit_types.find(i.first);
448  if(!type) {
449  continue;
450  }
451 
452  std::map<std::string, string_map> data;
454 
455  item["label"] = (formatter() << type->image() << "~RC(" << type->flag_rgb() << ">" << current_team_.color() << ")").str();
456  data.emplace("unit_image", item);
457 
458  // Note: the x here is a font::unicode_multiplication_sign
459  item["label"] = VGETTEXT("$count|× $name", {{"count", std::to_string(i.second)}, {"name", type->type_name()}});
460  data.emplace("unit_name", item);
461 
462  unit_list.add_row(data);
463  }
464 }
465 
466 } // namespace dialogs
467 } // namespace gui2
void add_damage_row(window &window, const std::string &type, const long long &damage, const long long &expected, const long long &turn_damage, const long long &turn_expected, const bool show_this_turn)
Add a row to the Damage table.
long long damage_inflicted
Definition: statistics.hpp:59
std::vector< double > hp_dist
Resulting probability distribution (might be not as large as max_hp)
const unit_type * find(const std::string &key, unit_type::BUILD_STATUS status=unit_type::FULL) const
Finds a unit_type by its id() and makes sure it is built to the specified level.
Definition: types.cpp:1267
Simple push button.
Definition: menu_button.hpp:41
static std::string get_probability_string(const double prob)
Various functions that implement attacks and attack calculations.
const std::string & side_name() const
Definition: team.hpp:306
int sum_str_int_map(const stats::str_int_map &m)
Definition: statistics.cpp:800
const std::string & flag_rgb() const
Definition: types.cpp:744
static std::string get_probability_string(const double prob)
This file contains the window object, this object is a top level container which has the event manage...
const statistics::stats campaign_
const statistics::levels scenarios_
long long turn_damage_taken
Definition: statistics.hpp:60
std::map< int, hitrate_t > hitrate_map
A type that maps chance-to-hit percentage to number of hits and strikes at that CTH.
Definition: statistics.hpp:71
int sum_cost_str_int_map(const stats::str_int_map &m)
Definition: statistics.cpp:810
unit_type_data unit_types
Definition: types.cpp:1525
Label showing a text.
Definition: label.hpp:32
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:272
void add_hits_row(window &window, 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 bool show_this_turn)
Add a row to the Hits table.
-file sdl_utils.hpp
str_int_map advanced_to
Definition: statistics.hpp:38
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition: listbox.cpp:250
A single unit type that the player may recruit.
Definition: types.hpp:42
Generic file dialog.
Definition: field-fwd.hpp:22
long long expected_damage_taken
Definition: statistics.hpp:81
long long turn_expected_damage_inflicted
Definition: statistics.hpp:82
void add_stat_row(window &window, const std::string &type, const statistics::stats::str_int_map &value, const bool has_cost=true)
void build_unit_type(const unit_type &ut, unit_type::BUILD_STATUS status) const
Makes sure the provided unit_type is built to the specified level.
Definition: types.hpp:373
virtual void set_label(const t_string &label)
The listbox class.
Definition: listbox.hpp:40
std::string span_color(const color_t &color)
Returns a Pango formatting string using the provided color_t object.
This class stores all the data for a single &#39;side&#39; (in game nomenclature).
Definition: team.hpp:44
static UNUSEDNOWARN std::string _(const char *str)
Definition: gettext.hpp:91
std::size_t size(const std::string &str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:86
void connect_signal_notify_modified(dispatcher &dispatcher, const signal_notification_function &signal)
Connects a signal handler for getting a notification upon modification.
Definition: dispatcher.cpp:248
virtual void pre_show(window &window) override
Inherited from modal_dialog.
static hitrate_table_element tally(const statistics::stats::hitrate_map &by_cth, const bool more_is_better)
static const int decimal_shift
Definition: statistics.hpp:75
This file contains the settings handling of the widget library.
std::ostringstream wrapper.
Definition: formatter.hpp:38
const t_string & type_name() const
The name of the unit in the current language setting.
Definition: types.hpp:135
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:125
levels level_stats(const std::string &save_id)
Returns a list of names and stats for each scenario in the current campaign.
Definition: statistics.cpp:722
str_int_map recalls
Definition: statistics.hpp:38
team * current_team_
Definition: move.cpp:313
hitrate_map turn_by_cth_taken
Definition: statistics.hpp:73
void set_values(const std::vector<::config > &values, unsigned selected=0)
void fight(combatant &opponent, bool levelup_considered=true)
Simulate a fight! Can be called multiple times for cumulative calculations.
Structure describing the statistics of a unit involved in the battle.
Definition: attack.hpp:48
str_int_map killed
Definition: statistics.hpp:38
const statistics::stats & current_stats()
Picks out the stats structure that was selected for displaying.
Various uncategorised dialogs.
hitrate_map turn_by_cth_inflicted
Definition: statistics.hpp:73
std::vector< const statistics::stats::str_int_map * > main_stat_table_
All combat-related info.
std::size_t i
Definition: function.cpp:933
str_int_map recruits
Definition: statistics.hpp:38
stats calculate_stats(const std::string &save_id)
Definition: statistics.cpp:695
str_int_map deaths
Definition: statistics.hpp:38
hitrate_map by_cth_taken
Definition: statistics.hpp:72
long long damage_taken
Definition: statistics.hpp:59
std::map< std::string, t_string > string_map
Definition: widget.hpp:24
grid & add_row(const string_map &item, const int index=-1)
When an item in the list is selected by the user we need to update the state.
Definition: listbox.cpp:66
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
long long turn_damage_inflicted
Definition: statistics.hpp:60
long long expected_damage_inflicted
Definition: statistics.hpp:81
const std::string & image() const
Definition: types.hpp:161
const t_string & get_label() const
const std::string & color() const
Definition: team.hpp:256
void on_primary_list_select(window &window)
color_t red_to_green(int val, bool for_text)
Return a color corresponding to the value val red for val=0 to green for val=100, passing by yellow...
std::map< std::string, int > str_int_map
Definition: statistics.hpp:37
const std::string unicode_em_dash
Definition: constants.cpp:40
Class to show the tips.
Definition: tooltip.cpp:71
long long turn_expected_damage_taken
Definition: statistics.hpp:82
static std::ostream & write_actual_and_expected(std::ostream &str, const long long actual, const double expected)
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:92
static map_location::DIRECTION n
base class of top level items, the only item which needs to store the final canvases to draw on ...
Definition: window.hpp:63
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:371
hitrate_map by_cth_inflicted
Definition: statistics.hpp:72