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