The Battle for Wesnoth  1.15.7+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().empty() ? "" : " (" + 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));
94 
95  //
96  // Set up primary stats list
97  //
98  listbox& stat_list = find_widget<listbox>(&window, "stats_list_main", false);
99 
102 
103  update_lists();
104 }
105 
107 {
108  return selection_index_ == 0 ? campaign_ : *scenarios_[selection_index_ - 1].second;
109 }
110 
112 {
113  listbox& stat_list = find_widget<listbox>(get_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  const std::string& type,
149  const long long& damage,
150  const long long& expected,
151  const long long& turn_damage,
152  const long long& turn_expected,
153  const bool show_this_turn)
154 {
155  listbox& damage_list = find_widget<listbox>(get_window(), "stats_list_damage", false);
156 
157  std::map<std::string, string_map> data;
159 
160  item["label"] = type;
161  data.emplace("damage_type", item);
162 
163  const int shift = statistics::stats::decimal_shift;
164 
165  const auto damage_str = [shift](long long damage, long long expected) {
166  const long long shifted = ((expected * 20) + shift) / (2 * shift);
167  std::ostringstream str;
168  write_actual_and_expected(str, damage, static_cast<double>(shifted) * 0.1);
169  return str.str();
170  };
171 
172  item["label"] = damage_str(damage, expected);
173  data.emplace("damage_overall", item);
174 
175  item["label"] = "";
176  data.emplace("overall_score", item);
177 
178  if(show_this_turn) {
179  label& this_turn_header = find_widget<label>(get_window(), "damage_this_turn_header", false);
180  this_turn_header.set_label(_("This Turn"));
181 
182  item["label"] = damage_str(turn_damage, turn_expected);
183  data.emplace("damage_this_turn", item);
184 
185  item["label"] = "";
186  data.emplace("this_turn_score", item);
187  } else {
188  // TODO: Setting the label to "" causes "This Turn" not to be drawn when changing back to the current scenario view, so set the label to " " (a single space) instead.
189  label& this_turn_header = find_widget<label>(get_window(), "damage_this_turn_header", false);
190  this_turn_header.set_label(" ");
191  }
192 
193  damage_list.add_row(data);
194 }
195 
196 // Custom type to allow tally() to return two values.
198 {
199  // The string with <actual number of hits>/<expected number of hits>
201  // The string with the a priori probability of that result
203  // The tooltip of the table cell - shows the actual (empirical) CTH
205 };
206 
207 // Return the strings to use in the "Hits" table, showing actual and expected number of hits.
208 static hitrate_table_element tally(const statistics::stats::hitrate_map& by_cth, const bool more_is_better)
209 {
210  unsigned int overall_hits = 0;
211  double expected_hits = 0;
212  unsigned int overall_strikes = 0;
213 
214  std::ostringstream str, str2, tooltip;
215 
216  tooltip << '\n' << '\n' << _("Actual hit rates, by chance to hit:");
217  if(by_cth.empty())
218  tooltip << '\n' << _("(no attacks have taken place yet)");
219  for(const auto& i : by_cth) {
220  int cth = i.first;
221  overall_hits += i.second.hits;
222  expected_hits += (cth * 0.01) * i.second.strikes;
223  overall_strikes += i.second.strikes;
224  tooltip << "\n" << cth << "%: "
225  << get_probability_string(i.second.hits/static_cast<double>(i.second.strikes))
226  << "% (N=" << i.second.strikes << ")";
227  }
228 
229  write_actual_and_expected(str, overall_hits, expected_hits);
230 
231  // Compute the a priori probability of this actual result, by simulating many attacks against a single defender.
232  {
233  config defender_cfg(
234  "id", "statistics_dialog_dummy_defender",
235  "hide_help", true,
236  "do_not_list", true,
237  "hitpoints", overall_strikes
238  );
239  unit_type defender_type(defender_cfg);
240  unit_types.build_unit_type(defender_type, unit_type::BUILD_STATUS::FULL);
241 
242  battle_context_unit_stats defender_bc(&defender_type, nullptr, false, nullptr, nullptr, 0 /* not used */);
243  std::unique_ptr<combatant> current_defender(new combatant(defender_bc));
244 
245  for(const auto& i : by_cth) {
246  int cth = i.first;
247  config attacker_cfg(
248  "id", "statistics_dialog_dummy_attacker" + std::to_string(cth),
249  "hide_help", true,
250  "do_not_list", true,
251  "hitpoints", 1
252  );
253  unit_type attacker_type(attacker_cfg);
254  unit_types.build_unit_type(attacker_type, unit_type::BUILD_STATUS::FULL);
255 
256  auto attack = std::make_shared<attack_type>(config(
257  "type", "blade",
258  "range", "melee",
259  "name", "dummy attack",
260  "damage", 1,
261  "number", i.second.strikes
262  ));
263 
264  battle_context_unit_stats attacker_bc(&attacker_type, attack, true, &defender_type, nullptr, 100 - cth);
265  defender_bc = battle_context_unit_stats(&defender_type, nullptr, false, &attacker_type, attack, 0 /* not used */);
266 
267  // Update current_defender with the new defender_bc.
268  current_defender.reset(new combatant(*current_defender, defender_bc));
269 
270  combatant attacker(attacker_bc);
271  attacker.fight(*current_defender);
272  }
273 
274  const std::vector<double>& final_hp_dist = current_defender->hp_dist;
275  const auto chance_of_exactly_N_hits = [&final_hp_dist](int n) { return final_hp_dist[final_hp_dist.size() - 1 - n]; };
276 
277  // The a priori probability of scoring less hits than the actual number of hits
278  // aka "percentile" or "p-value"
279  double probability_lt = 0.0;
280  for(unsigned int i = 0; i < overall_hits; ++i) {
281  probability_lt += chance_of_exactly_N_hits(i);
282  }
283  // The a priori probability of scoring exactly the actual number of hits
284  double probability_eq = chance_of_exactly_N_hits(overall_hits);
285  // The a priori probability of scoring more hits than the actual number of hits
286  double probability_gt = 1.0 - (probability_lt + probability_eq);
287 
288  if(overall_strikes == 0) {
289  // Start of turn
290  str2 << font::unicode_em_dash;
291  } else {
292  const auto add_probability = [&str2](double probability, bool more_is_better) {
293  str2 << font::span_color(game_config::red_to_green((more_is_better ? probability : 1.0 - probability) * 100.0, true))
294  << get_probability_string(probability) << "</span>";
295  };
296 
297  // Take the average. At the end of a scenario or a campaign the sum of
298  // probability_lt+probability_gt is very close to 1.0 so the percentile is
299  // approximately equal to probability_lt.
300  const double percentile = (probability_lt + (1.0 - probability_gt)) / 2.0;
301  add_probability(percentile, more_is_better);
302  }
303  }
304 
305  return hitrate_table_element{str.str(), str2.str(), tooltip.str()};
306 }
307 
309  const std::string& type,
310  const bool more_is_better,
311  const statistics::stats::hitrate_map& by_cth,
312  const statistics::stats::hitrate_map& turn_by_cth,
313  const bool show_this_turn)
314 {
315  listbox& hits_list = find_widget<listbox>(get_window(), "stats_list_hits", false);
316 
317  std::map<std::string, string_map> data;
319 
320  hitrate_table_element element;
321 
322  item["label"] = type;
323  data.emplace("hits_type", item);
324 
325  const auto tooltip_static_part = _(
326  "stats dialog^Difference of actual outcome to expected outcome, as a percentage.\n"
327  "The first number in parentheses is the expected number of hits inflicted/taken.\n"
328  "The sum (or difference) of the two numbers in parentheses is the actual number of hits inflicted/taken.");
329  element = tally(by_cth, more_is_better);
330  item["tooltip"] = tooltip_static_part + element.tooltip;
331  item["label"] = element.hitrate_str;
332  data.emplace("hits_overall", item);
333 
334  // Don't set the tooltip; it's set in WML.
335  data.emplace("overall_score", string_map { { "label", element.pvalue_str } });
336 
337  if(show_this_turn) {
338  label& this_turn_header = find_widget<label>(get_window(), "hits_this_turn_header", false);
339  this_turn_header.set_label(_("This Turn"));
340 
341  element = tally(turn_by_cth, more_is_better);
342  item["tooltip"] = tooltip_static_part + element.tooltip;
343  item["label"] = element.hitrate_str;
344  data.emplace("hits_this_turn", item);
345 
346  // Don't set the tooltip; it's set in WML.
347  data.emplace("this_turn_score", string_map { { "label", element.pvalue_str } });
348  } else {
349  // TODO: Setting the label to "" causes "This Turn" not to be drawn when changing back to the current scenario view, so set the label to " " (a single space) instead.
350  label& this_turn_header = find_widget<label>(get_window(), "hits_this_turn_header", false);
351  this_turn_header.set_label(" ");
352  }
353 
354  hits_list.add_row(data);
355 }
356 
358 {
359  //
360  // Update primary stats list
361  //
362  listbox& stat_list = find_widget<listbox>(get_window(), "stats_list_main", false);
363  const int last_selected_stat_row = stat_list.get_selected_row();
364 
365  stat_list.clear();
366  main_stat_table_.clear();
367 
368  const statistics::stats& stats = current_stats();
369 
370  add_stat_row(_("Recruits"), stats.recruits);
371  add_stat_row(_("Recalls"), stats.recalls);
372  add_stat_row(_("Advancements"), stats.advanced_to, false);
373  add_stat_row(_("Losses"), stats.deaths);
374  add_stat_row(_("Kills"), stats.killed);
375 
376  // Reselect previously selected row. Do this *before* calling on_primary_list_select.
377  if(last_selected_stat_row != -1) {
378  stat_list.select_row(last_selected_stat_row);
379  }
380 
381  // Update unit count list
383 
384  //
385  // Update damage stats list
386  //
387  const bool show_this_turn = selection_index_ == scenarios_.size();
388 
389  listbox& damage_list = find_widget<listbox>(get_window(), "stats_list_damage", false);
390 
391  damage_list.clear();
392 
393  listbox& hits_list = find_widget<listbox>(get_window(), "stats_list_hits", false);
394  hits_list.clear();
395 
396  add_damage_row(_("Inflicted"),
397  stats.damage_inflicted,
399  stats.turn_damage_inflicted,
401  show_this_turn
402  );
403  add_hits_row(_("Inflicted"), true,
404  stats.by_cth_inflicted,
405  stats.turn_by_cth_inflicted,
406  show_this_turn
407  );
408 
409  add_damage_row(_("Taken"),
410  stats.damage_taken,
411  stats.expected_damage_taken,
412  stats.turn_damage_taken,
414  show_this_turn
415  );
416  add_hits_row(_("Taken"), false,
417  stats.by_cth_taken,
418  stats.turn_by_cth_taken,
419  show_this_turn
420  );
421 }
422 
424 {
425  const std::size_t new_index = find_widget<menu_button>(get_window(), "scenario_menu", false).get_value();
426 
427  if(selection_index_ != new_index) {
428  selection_index_ = new_index;
429  update_lists();
430  }
431 }
432 
434 {
435  const int selected_row = find_widget<listbox>(get_window(), "stats_list_main", false).get_selected_row();
436  if(selected_row == -1) {
437  return;
438  }
439 
440  listbox& unit_list = find_widget<listbox>(get_window(), "stats_list_units", false);
441 
442  unit_list.clear();
443 
444  for(const auto& i : *main_stat_table_[selected_row]) {
445  const unit_type* type = unit_types.find(i.first);
446  if(!type) {
447  continue;
448  }
449 
450  std::map<std::string, string_map> data;
452 
453  item["label"] = (formatter() << type->image() << "~RC(" << type->flag_rgb() << ">" << current_team_.color() << ")").str();
454  data.emplace("unit_image", item);
455 
456  // Note: the x here is a font::unicode_multiplication_sign
457  item["label"] = VGETTEXT("$count|× $name", {{"count", std::to_string(i.second)}, {"name", type->type_name()}});
458  data.emplace("unit_name", item);
459 
460  unit_list.add_row(data);
461  }
462 }
463 
464 } // namespace dialogs
465 } // namespace gui2
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:1159
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:308
int sum_str_int_map(const stats::str_int_map &m)
Definition: statistics.cpp:801
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 bool show_this_turn)
Add a row to the Hits table.
const std::string & flag_rgb() const
Definition: types.cpp:689
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:811
unit_type_data unit_types
Definition: types.cpp:1378
window * get_window() const
Returns a pointer to the dialog&#39;s window.
Label showing a text.
Definition: label.hpp:32
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:276
std::string str
Definition: statement.cpp:110
-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:251
A single unit type that the player may recruit.
Definition: types.hpp:44
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 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.cpp:1151
virtual void set_label(const t_string &label)
The listbox class.
Definition: listbox.hpp:42
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:100
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:186
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:136
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:126
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:723
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.
void add_stat_row(const std::string &type, const statistics::stats::str_int_map &value, const bool has_cost=true)
void add_damage_row(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.
Structure describing the statistics of a unit involved in the battle.
Definition: attack.hpp:50
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:696
u64 size
Definition: statement.cpp:80
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:26
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:67
#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:166
const t_string & get_label() const
const std::string & color() const
Definition: team.hpp:256
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:68
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:62
std::pair< std::string, unsigned > item
Definition: help_impl.hpp:384
hitrate_map by_cth_inflicted
Definition: statistics.hpp:72
auto * ss
Definition: result_set.cpp:281