The Battle for Wesnoth  1.19.17+dev
statistics_dialog.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2016 - 2025
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 "serialization/markup.hpp"
22 #include "formatter.hpp"
23 #include "formula/string_utils.hpp"
24 #include "gettext.hpp"
25 #include "gui/widgets/label.hpp"
26 #include "gui/widgets/listbox.hpp"
28 #include "gui/widgets/window.hpp"
29 #include "team.hpp"
30 #include "units/types.hpp"
31 
32 #include <functional>
33 #include <iomanip>
34 #include <memory>
35 
36 // TODO duplicated from attack_predictions.cpp
37 static std::string get_probability_string(const double prob)
38 {
39  std::ostringstream ss;
40 
41  if(prob > 0.9995) {
42  ss << "100";
43  } else {
44  ss << std::fixed << std::setprecision(1) << 100.0 * prob;
45  }
46 
47  return ss.str();
48 }
49 
50 namespace gui2::dialogs
51 {
52 REGISTER_DIALOG(statistics_dialog)
53 
54 statistics_dialog::statistics_dialog(statistics_t& statistics, const team& current_team)
55  : modal_dialog(window_id())
56  , current_team_(current_team)
57  , campaign_(statistics.calculate_stats(current_team.save_id_or_number()))
58  , scenarios_(statistics.level_stats(current_team.save_id_or_number()))
59  , selection_index_(scenarios_.size()) // The extra All Scenarios menu entry makes size() a valid initial index.
60  , main_stat_table_()
61 {
62 }
63 
65 {
66  //
67  // Set title
68  //
69  label& title = find_widget<label>("title");
70  title.set_label((formatter() << title.get_label() << (current_team_.side_name().empty() ? "" : " (" + current_team_.side_name() + ")")).str());
71 
72  //
73  // Set up scenario menu
74  //
75  std::vector<config> menu_items;
76 
77  // Keep this first!
78  menu_items.emplace_back("label", _("All Scenarios"));
79 
80  for(const auto& scenario : scenarios_) {
81  menu_items.emplace_back("label", *scenario.first);
82  }
83 
84  menu_button& scenario_menu = find_widget<menu_button>("scenario_menu");
85 
86  scenario_menu.set_values(menu_items, selection_index_);
87 
88  connect_signal_notify_modified(scenario_menu,
89  std::bind(&statistics_dialog::on_scenario_select, this));
90 
91  //
92  // Set up primary stats list
93  //
94  listbox& stat_list = find_widget<listbox>("stats_list_main");
95 
98 
99  update_lists();
100 }
101 
103 {
104  return selection_index_ == 0 ? campaign_ : *scenarios_[selection_index_ - 1].second;
105 }
106 
107 void statistics_dialog::add_stat_row(const std::string& type, const statistics_t::stats::str_int_map& value, const bool has_cost)
108 {
109  find_widget<listbox>("stats_list_main").add_row(widget_data{
110  { "stat_type", {
111  { "label", type }
112  }},
113  { "stat_detail", {
114  { "label", std::to_string(statistics_t::sum_str_int_map(value)) }
115  }},
116  { "stat_cost", {
117  { "label", has_cost
118  ? std::to_string(statistics_t::sum_cost_str_int_map(value))
120  }},
121  });
122 
123  main_stat_table_.push_back(&value);
124 }
125 
126 // Generate the string for the "A + B" column of the damage and hits tables.
127 static std::ostream& write_actual_and_expected(std::ostream& str, const long long actual, const double expected)
128 {
129  // 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.
130  if(expected == 0) {
131  str << "+0% (0 + 0)";
132  } else {
133  str << (formatter() << std::showpos << std::round((actual - expected) * 100 / expected) << "% (").str();
134  str << expected << (actual >= expected ? " + " : " − ")
135  << static_cast<unsigned int>(std::round(std::abs(expected - actual)));
136  str << ')';
137  }
138  return str;
139 }
140 
142  const std::string& type,
143  const long long& damage,
144  const long long& expected,
145  const long long& turn_damage,
146  const long long& turn_expected,
147  const bool show_this_turn)
148 {
149  listbox& damage_list = find_widget<listbox>("stats_list_damage");
150 
152  widget_item item;
153 
154  item["label"] = type;
155  data.emplace("damage_type", item);
156 
157  static const int shift = statistics_t::stats::decimal_shift;
158 
159  const auto damage_str = [](long long damage, long long expected) {
160  const long long shifted = ((expected * 20) + shift) / (2 * shift);
161  std::ostringstream str;
162  write_actual_and_expected(str, damage, static_cast<double>(shifted) * 0.1);
163  return str.str();
164  };
165 
166  item["label"] = damage_str(damage, expected);
167  data.emplace("damage_overall", item);
168 
169  item["label"] = "";
170  data.emplace("overall_score", item);
171 
172  if(show_this_turn) {
173  label& this_turn_header = find_widget<label>("damage_this_turn_header");
174  this_turn_header.set_label(_("This Turn"));
175 
176  item["label"] = damage_str(turn_damage, turn_expected);
177  data.emplace("damage_this_turn", item);
178 
179  item["label"] = "";
180  data.emplace("this_turn_score", item);
181  } else {
182  // 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.
183  label& this_turn_header = find_widget<label>("damage_this_turn_header");
184  this_turn_header.set_label(" ");
185  }
186 
187  damage_list.add_row(data);
188 }
189 
190 // Custom type to allow tally() to return two values.
192 {
193  // The string with <actual number of hits>/<expected number of hits>
194  std::string hitrate_str;
195  // The string with the a priori probability of that result
196  std::string pvalue_str;
197  // The tooltip of the table cell - shows the actual (empirical) CTH
198  std::string tooltip;
199 };
200 
201 // Return the strings to use in the "Hits" table, showing actual and expected number of hits.
202 static hitrate_table_element tally(const statistics_t::stats::hitrate_map& by_cth, const bool more_is_better)
203 {
204  unsigned int overall_hits = 0;
205  double expected_hits = 0;
206  unsigned int overall_strikes = 0;
207 
208  std::ostringstream str, str2, tooltip;
209 
210  tooltip << '\n' << '\n' << _("Actual hit rates, by chance to hit:");
211  if(by_cth.empty())
212  tooltip << '\n' << _("(no attacks have taken place yet)");
213  for(const auto& i : by_cth) {
214  int cth = i.first;
215  overall_hits += i.second.hits;
216  expected_hits += (cth * 0.01) * i.second.strikes;
217  overall_strikes += i.second.strikes;
218  tooltip << "\n" << cth << "%: "
219  << get_probability_string(i.second.hits/static_cast<double>(i.second.strikes))
220  << "% (N=" << i.second.strikes << ")";
221  }
222 
223  write_actual_and_expected(str, overall_hits, expected_hits);
224 
225  // Compute the a priori probability of this actual result, by simulating many attacks against a single defender.
226  {
227  auto defender_bc = battle_context_unit_stats(0, 0, overall_strikes, overall_strikes, 0);
228  auto current_defender = std::make_unique<combatant>(defender_bc);
229 
230  for(const auto& i : by_cth) {
231  int cth = i.first;
232 
233  auto attacker_bc= battle_context_unit_stats(1, i.second.strikes, 1, 1, cth);
234  defender_bc = battle_context_unit_stats(0, 0, overall_strikes, overall_strikes, 0);
235 
236  // Update current_defender with the new defender_bc.
237  current_defender.reset(new combatant(*current_defender, defender_bc));
238 
239  combatant attacker(attacker_bc);
240  attacker.fight(*current_defender);
241  }
242 
243  const std::vector<double>& final_hp_dist = current_defender->hp_dist;
244  const auto chance_of_exactly_N_hits = [&final_hp_dist](int n) { return final_hp_dist[final_hp_dist.size() - 1 - n]; };
245 
246  // The a priori probability of scoring less hits than the actual number of hits
247  // aka "percentile" or "p-value"
248  double probability_lt = 0.0;
249  for(unsigned int i = 0; i < overall_hits; ++i) {
250  probability_lt += chance_of_exactly_N_hits(i);
251  }
252  // The a priori probability of scoring exactly the actual number of hits
253  double probability_eq = chance_of_exactly_N_hits(overall_hits);
254  // The a priori probability of scoring more hits than the actual number of hits
255  double probability_gt = 1.0 - (probability_lt + probability_eq);
256 
257  if(overall_strikes == 0) {
258  // Start of turn
259  str2 << font::unicode_em_dash;
260  } else {
261  const auto add_probability = [&str2](double probability, bool more_is_better) {
262  str2 << markup::span_color(
263  game_config::red_to_green((more_is_better ? probability : 1.0 - probability) * 100.0, true),
264  get_probability_string(probability));
265  };
266 
267  // Take the average. At the end of a scenario or a campaign the sum of
268  // probability_lt+probability_gt is very close to 1.0 so the percentile is
269  // approximately equal to probability_lt.
270  const double percentile = (probability_lt + (1.0 - probability_gt)) / 2.0;
271  add_probability(percentile, more_is_better);
272  }
273  }
274 
275  return hitrate_table_element{str.str(), str2.str(), tooltip.str()};
276 }
277 
279  const std::string& type,
280  const bool more_is_better,
281  const statistics_t::stats::hitrate_map& by_cth,
282  const statistics_t::stats::hitrate_map& turn_by_cth,
283  const bool show_this_turn)
284 {
285  listbox& hits_list = find_widget<listbox>("stats_list_hits");
286 
288  widget_item item;
289 
290  hitrate_table_element element;
291 
292  item["label"] = type;
293  data.emplace("hits_type", item);
294 
295  const auto tooltip_static_part = _(
296  "stats dialog^Difference of actual outcome to expected outcome, as a percentage.\n"
297  "The first number in parentheses is the expected number of hits inflicted/taken.\n"
298  "The sum (or difference) of the two numbers in parentheses is the actual number of hits inflicted/taken.");
299  element = tally(by_cth, more_is_better);
300  item["tooltip"] = tooltip_static_part + element.tooltip;
301  item["label"] = element.hitrate_str;
302  data.emplace("hits_overall", item);
303 
304  // Don't set the tooltip; it's set in WML.
305  data.emplace("overall_score", widget_item { { "label", element.pvalue_str } });
306 
307  if(show_this_turn) {
308  label& this_turn_header = find_widget<label>("hits_this_turn_header");
309  this_turn_header.set_label(_("This Turn"));
310 
311  element = tally(turn_by_cth, more_is_better);
312  item["tooltip"] = tooltip_static_part + element.tooltip;
313  item["label"] = element.hitrate_str;
314  data.emplace("hits_this_turn", item);
315 
316  // Don't set the tooltip; it's set in WML.
317  data.emplace("this_turn_score", widget_item { { "label", element.pvalue_str } });
318  } else {
319  // 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.
320  label& this_turn_header = find_widget<label>("hits_this_turn_header");
321  this_turn_header.set_label(" ");
322  }
323 
324  hits_list.add_row(data);
325 }
326 
328 {
329  //
330  // Update primary stats list
331  //
332  listbox& stat_list = find_widget<listbox>("stats_list_main");
333  const int last_selected_stat_row = stat_list.get_selected_row();
334 
335  stat_list.clear();
336  main_stat_table_.clear();
337 
338  const statistics_t::stats& stats = current_stats();
339 
340  add_stat_row(_("stats^Recruits"), stats.recruits);
341  add_stat_row(_("Recalls"), stats.recalls);
342  add_stat_row(_("Advancements"), stats.advanced_to, false);
343  add_stat_row(_("Losses"), stats.deaths);
344  add_stat_row(_("Kills"), stats.killed);
345 
346  // Reselect previously selected row. Do this *before* calling on_primary_list_select.
347  if(last_selected_stat_row != -1) {
348  stat_list.select_row(last_selected_stat_row);
349  }
350 
351  // Update unit count list
353 
354  //
355  // Update damage stats list
356  //
357  const bool show_this_turn = selection_index_ == scenarios_.size();
358 
359  listbox& damage_list = find_widget<listbox>("stats_list_damage");
360 
361  damage_list.clear();
362 
363  listbox& hits_list = find_widget<listbox>("stats_list_hits");
364  hits_list.clear();
365 
366  add_damage_row(_("Inflicted"),
367  stats.damage_inflicted,
369  stats.turn_damage_inflicted,
371  show_this_turn
372  );
373  add_hits_row(_("Inflicted"), true,
374  stats.by_cth_inflicted,
375  stats.turn_by_cth_inflicted,
376  show_this_turn
377  );
378 
379  add_damage_row(_("Taken"),
380  stats.damage_taken,
381  stats.expected_damage_taken,
382  stats.turn_damage_taken,
384  show_this_turn
385  );
386  add_hits_row(_("Taken"), false,
387  stats.by_cth_taken,
388  stats.turn_by_cth_taken,
389  show_this_turn
390  );
391 }
392 
394 {
395  const std::size_t new_index = find_widget<menu_button>("scenario_menu").get_value();
396 
397  if(selection_index_ != new_index) {
398  selection_index_ = new_index;
399  update_lists();
400  }
401 }
402 
404 {
405  const int selected_row = find_widget<listbox>("stats_list_main").get_selected_row();
406  if(selected_row == -1) {
407  return;
408  }
409 
410  listbox& unit_list = find_widget<listbox>("stats_list_units");
411 
412  unit_list.clear();
413 
414  for(const auto& i : *main_stat_table_[selected_row]) {
415  const unit_type* type = unit_types.find(i.first);
416  if(!type) {
417  continue;
418  }
419 
421  widget_item item;
422 
423  item["label"] = (formatter() << type->image() << "~RC(" << type->flag_rgb() << ">" << current_team_.color() << ")").str();
424  data.emplace("unit_image", item);
425 
426  // Note: the x here is a font::unicode_multiplication_sign
427  item["label"] = VGETTEXT("$count|× $name", {{"count", std::to_string(i.second)}, {"name", type->type_name()}});
428  data.emplace("unit_name", item);
429 
430  unit_list.add_row(data);
431  }
432 }
433 
434 } // namespace dialogs
Various functions that implement attacks and attack calculations.
team * current_team_
Definition: move.cpp:407
std::ostringstream wrapper.
Definition: formatter.hpp:40
Abstract base class for all modal dialogs.
const statistics_t::stats & current_stats()
Picks out the stats structure that was selected for displaying.
const statistics_t::levels scenarios_
virtual void pre_show() override
Actions to be taken before showing the window.
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_
At the moment two kinds of tips are known:
Definition: tooltip.cpp:41
The listbox class.
Definition: listbox.hpp:41
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:92
bool select_row(const unsigned row, const bool select=true)
Selects a row.
Definition: listbox.cpp:267
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:147
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:289
void set_values(const std::vector<::config > &values, unsigned selected=0)
const t_string & get_label() const
virtual void set_label(const t_string &text)
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:74
const std::string & color() const
Definition: team.hpp:280
const std::string & side_name() const
Definition: team.hpp:331
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:1259
A single unit type that the player may recruit.
Definition: types.hpp:43
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
std::size_t i
Definition: function.cpp:1032
static std::string _(const char *str)
Definition: gettext.hpp:97
std::string tooltip
Shown when hovering over an entry in the filter's drop-down list.
Definition: manager.cpp:203
This file contains the window object, this object is a top level container which has the event manage...
const std::string unicode_em_dash
Definition: constants.cpp:44
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....
REGISTER_DIALOG(editor_edit_unit)
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:189
std::map< std::string, widget_item > widget_data
Definition: widget.hpp:36
std::map< std::string, t_string > widget_item
Definition: widget.hpp:33
std::string span_color(const color_t &color, Args &&... data)
Applies Pango markup to the input specifying its display color.
Definition: markup.hpp:110
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:81
std::string_view data
Definition: picture.cpp:188
static std::string get_probability_string(const double prob)
Structure describing the statistics of a unit involved in the battle.
Definition: attack.hpp:54
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:1499