The Battle for Wesnoth  1.19.10+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  config defender_cfg(
228  "id", "statistics_dialog_dummy_defender",
229  "hide_help", true,
230  "do_not_list", true,
231  "hitpoints", overall_strikes
232  );
233  unit_type defender_type(defender_cfg);
234  unit_types.build_unit_type(defender_type, unit_type::BUILD_STATUS::FULL);
235 
236  battle_context_unit_stats defender_bc(&defender_type, nullptr, false, nullptr, nullptr, 0 /* not used */);
237  auto current_defender = std::make_unique<combatant>(defender_bc);
238 
239  for(const auto& i : by_cth) {
240  int cth = i.first;
241  config attacker_cfg(
242  "id", "statistics_dialog_dummy_attacker" + std::to_string(cth),
243  "hide_help", true,
244  "do_not_list", true,
245  "hitpoints", 1
246  );
247  unit_type attacker_type(attacker_cfg);
248  unit_types.build_unit_type(attacker_type, unit_type::BUILD_STATUS::FULL);
249 
250  auto attack = std::make_shared<attack_type>(config(
251  "type", "blade",
252  "range", "melee",
253  "name", "dummy attack",
254  "damage", 1,
255  "number", i.second.strikes
256  ));
257 
258  battle_context_unit_stats attacker_bc(&attacker_type, attack, true, &defender_type, nullptr, 100 - cth);
259  defender_bc = battle_context_unit_stats(&defender_type, nullptr, false, &attacker_type, attack, 0 /* not used */);
260 
261  // Update current_defender with the new defender_bc.
262  current_defender.reset(new combatant(*current_defender, defender_bc));
263 
264  combatant attacker(attacker_bc);
265  attacker.fight(*current_defender);
266  }
267 
268  const std::vector<double>& final_hp_dist = current_defender->hp_dist;
269  const auto chance_of_exactly_N_hits = [&final_hp_dist](int n) { return final_hp_dist[final_hp_dist.size() - 1 - n]; };
270 
271  // The a priori probability of scoring less hits than the actual number of hits
272  // aka "percentile" or "p-value"
273  double probability_lt = 0.0;
274  for(unsigned int i = 0; i < overall_hits; ++i) {
275  probability_lt += chance_of_exactly_N_hits(i);
276  }
277  // The a priori probability of scoring exactly the actual number of hits
278  double probability_eq = chance_of_exactly_N_hits(overall_hits);
279  // The a priori probability of scoring more hits than the actual number of hits
280  double probability_gt = 1.0 - (probability_lt + probability_eq);
281 
282  if(overall_strikes == 0) {
283  // Start of turn
284  str2 << font::unicode_em_dash;
285  } else {
286  const auto add_probability = [&str2](double probability, bool more_is_better) {
287  str2 << markup::span_color(
288  game_config::red_to_green((more_is_better ? probability : 1.0 - probability) * 100.0, true),
289  get_probability_string(probability));
290  };
291 
292  // Take the average. At the end of a scenario or a campaign the sum of
293  // probability_lt+probability_gt is very close to 1.0 so the percentile is
294  // approximately equal to probability_lt.
295  const double percentile = (probability_lt + (1.0 - probability_gt)) / 2.0;
296  add_probability(percentile, more_is_better);
297  }
298  }
299 
300  return hitrate_table_element{str.str(), str2.str(), tooltip.str()};
301 }
302 
304  const std::string& type,
305  const bool more_is_better,
306  const statistics_t::stats::hitrate_map& by_cth,
307  const statistics_t::stats::hitrate_map& turn_by_cth,
308  const bool show_this_turn)
309 {
310  listbox& hits_list = find_widget<listbox>("stats_list_hits");
311 
313  widget_item item;
314 
315  hitrate_table_element element;
316 
317  item["label"] = type;
318  data.emplace("hits_type", item);
319 
320  const auto tooltip_static_part = _(
321  "stats dialog^Difference of actual outcome to expected outcome, as a percentage.\n"
322  "The first number in parentheses is the expected number of hits inflicted/taken.\n"
323  "The sum (or difference) of the two numbers in parentheses is the actual number of hits inflicted/taken.");
324  element = tally(by_cth, more_is_better);
325  item["tooltip"] = tooltip_static_part + element.tooltip;
326  item["label"] = element.hitrate_str;
327  data.emplace("hits_overall", item);
328 
329  // Don't set the tooltip; it's set in WML.
330  data.emplace("overall_score", widget_item { { "label", element.pvalue_str } });
331 
332  if(show_this_turn) {
333  label& this_turn_header = find_widget<label>("hits_this_turn_header");
334  this_turn_header.set_label(_("This Turn"));
335 
336  element = tally(turn_by_cth, more_is_better);
337  item["tooltip"] = tooltip_static_part + element.tooltip;
338  item["label"] = element.hitrate_str;
339  data.emplace("hits_this_turn", item);
340 
341  // Don't set the tooltip; it's set in WML.
342  data.emplace("this_turn_score", widget_item { { "label", element.pvalue_str } });
343  } else {
344  // 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.
345  label& this_turn_header = find_widget<label>("hits_this_turn_header");
346  this_turn_header.set_label(" ");
347  }
348 
349  hits_list.add_row(data);
350 }
351 
353 {
354  //
355  // Update primary stats list
356  //
357  listbox& stat_list = find_widget<listbox>("stats_list_main");
358  const int last_selected_stat_row = stat_list.get_selected_row();
359 
360  stat_list.clear();
361  main_stat_table_.clear();
362 
363  const statistics_t::stats& stats = current_stats();
364 
365  add_stat_row(_("stats^Recruits"), stats.recruits);
366  add_stat_row(_("Recalls"), stats.recalls);
367  add_stat_row(_("Advancements"), stats.advanced_to, false);
368  add_stat_row(_("Losses"), stats.deaths);
369  add_stat_row(_("Kills"), stats.killed);
370 
371  // Reselect previously selected row. Do this *before* calling on_primary_list_select.
372  if(last_selected_stat_row != -1) {
373  stat_list.select_row(last_selected_stat_row);
374  }
375 
376  // Update unit count list
378 
379  //
380  // Update damage stats list
381  //
382  const bool show_this_turn = selection_index_ == scenarios_.size();
383 
384  listbox& damage_list = find_widget<listbox>("stats_list_damage");
385 
386  damage_list.clear();
387 
388  listbox& hits_list = find_widget<listbox>("stats_list_hits");
389  hits_list.clear();
390 
391  add_damage_row(_("Inflicted"),
392  stats.damage_inflicted,
394  stats.turn_damage_inflicted,
396  show_this_turn
397  );
398  add_hits_row(_("Inflicted"), true,
399  stats.by_cth_inflicted,
400  stats.turn_by_cth_inflicted,
401  show_this_turn
402  );
403 
404  add_damage_row(_("Taken"),
405  stats.damage_taken,
406  stats.expected_damage_taken,
407  stats.turn_damage_taken,
409  show_this_turn
410  );
411  add_hits_row(_("Taken"), false,
412  stats.by_cth_taken,
413  stats.turn_by_cth_taken,
414  show_this_turn
415  );
416 }
417 
419 {
420  const std::size_t new_index = find_widget<menu_button>("scenario_menu").get_value();
421 
422  if(selection_index_ != new_index) {
423  selection_index_ = new_index;
424  update_lists();
425  }
426 }
427 
429 {
430  const int selected_row = find_widget<listbox>("stats_list_main").get_selected_row();
431  if(selected_row == -1) {
432  return;
433  }
434 
435  listbox& unit_list = find_widget<listbox>("stats_list_units");
436 
437  unit_list.clear();
438 
439  for(const auto& i : *main_stat_table_[selected_row]) {
440  const unit_type* type = unit_types.find(i.first);
441  if(!type) {
442  continue;
443  }
444 
446  widget_item item;
447 
448  item["label"] = (formatter() << type->image() << "~RC(" << type->flag_rgb() << ">" << current_team_.color() << ")").str();
449  data.emplace("unit_image", item);
450 
451  // Note: the x here is a font::unicode_multiplication_sign
452  item["label"] = VGETTEXT("$count|× $name", {{"count", std::to_string(i.second)}, {"name", type->type_name()}});
453  data.emplace("unit_name", item);
454 
455  unit_list.add_row(data);
456  }
457 }
458 
459 } // namespace dialogs
Various functions that implement attacks and attack calculations.
team * current_team_
Definition: move.cpp:407
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:158
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:280
void clear()
Removes all the rows in the listbox, clearing it.
Definition: listbox.cpp:153
int get_selected_row() const
Returns the first selected row.
Definition: listbox.cpp:305
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:75
const std::string & color() const
Definition: team.hpp:247
const std::string & side_name() const
Definition: team.hpp:298
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:1265
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:1257
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:1030
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:116
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition: unicode.cpp:85
std::string_view data
Definition: picture.cpp:178
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:1504