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