The Battle for Wesnoth  1.19.7+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 "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  listbox& stat_list = find_widget<listbox>("stats_list_main");
110 
112  widget_item item;
113 
114  item["label"] = type;
115  data.emplace("stat_type", item);
116 
117  item["label"] = std::to_string(statistics_t::sum_str_int_map(value));
118  data.emplace("stat_detail", item);
119 
120  item["label"] = has_cost ? std::to_string(statistics_t::sum_cost_str_int_map(value)) : font::unicode_em_dash;
121  data.emplace("stat_cost", item);
122 
123  stat_list.add_row(data);
124 
125  main_stat_table_.push_back(&value);
126 }
127 
128 // Generate the string for the "A + B" column of the damage and hits tables.
129 static std::ostream& write_actual_and_expected(std::ostream& str, const long long actual, const double expected)
130 {
131  // 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.
132  if(expected == 0) {
133  str << "+0% (0 + 0)";
134  } else {
135  str << (formatter() << std::showpos << std::round((actual - expected) * 100 / expected) << "% (").str();
136  str << expected << (actual >= expected ? " + " : " − ")
137  << static_cast<unsigned int>(std::round(std::abs(expected - actual)));
138  str << ')';
139  }
140  return str;
141 }
142 
144  const std::string& type,
145  const long long& damage,
146  const long long& expected,
147  const long long& turn_damage,
148  const long long& turn_expected,
149  const bool show_this_turn)
150 {
151  listbox& damage_list = find_widget<listbox>("stats_list_damage");
152 
154  widget_item item;
155 
156  item["label"] = type;
157  data.emplace("damage_type", item);
158 
159  static const int shift = statistics_t::stats::decimal_shift;
160 
161  const auto damage_str = [](long long damage, long long expected) {
162  const long long shifted = ((expected * 20) + shift) / (2 * shift);
163  std::ostringstream str;
164  write_actual_and_expected(str, damage, static_cast<double>(shifted) * 0.1);
165  return str.str();
166  };
167 
168  item["label"] = damage_str(damage, expected);
169  data.emplace("damage_overall", item);
170 
171  item["label"] = "";
172  data.emplace("overall_score", item);
173 
174  if(show_this_turn) {
175  label& this_turn_header = find_widget<label>("damage_this_turn_header");
176  this_turn_header.set_label(_("This Turn"));
177 
178  item["label"] = damage_str(turn_damage, turn_expected);
179  data.emplace("damage_this_turn", item);
180 
181  item["label"] = "";
182  data.emplace("this_turn_score", item);
183  } else {
184  // 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.
185  label& this_turn_header = find_widget<label>("damage_this_turn_header");
186  this_turn_header.set_label(" ");
187  }
188 
189  damage_list.add_row(data);
190 }
191 
192 // Custom type to allow tally() to return two values.
194 {
195  // The string with <actual number of hits>/<expected number of hits>
196  std::string hitrate_str;
197  // The string with the a priori probability of that result
198  std::string pvalue_str;
199  // The tooltip of the table cell - shows the actual (empirical) CTH
200  std::string tooltip;
201 };
202 
203 // Return the strings to use in the "Hits" table, showing actual and expected number of hits.
204 static hitrate_table_element tally(const statistics_t::stats::hitrate_map& by_cth, const bool more_is_better)
205 {
206  unsigned int overall_hits = 0;
207  double expected_hits = 0;
208  unsigned int overall_strikes = 0;
209 
210  std::ostringstream str, str2, tooltip;
211 
212  tooltip << '\n' << '\n' << _("Actual hit rates, by chance to hit:");
213  if(by_cth.empty())
214  tooltip << '\n' << _("(no attacks have taken place yet)");
215  for(const auto& i : by_cth) {
216  int cth = i.first;
217  overall_hits += i.second.hits;
218  expected_hits += (cth * 0.01) * i.second.strikes;
219  overall_strikes += i.second.strikes;
220  tooltip << "\n" << cth << "%: "
221  << get_probability_string(i.second.hits/static_cast<double>(i.second.strikes))
222  << "% (N=" << i.second.strikes << ")";
223  }
224 
225  write_actual_and_expected(str, overall_hits, expected_hits);
226 
227  // Compute the a priori probability of this actual result, by simulating many attacks against a single defender.
228  {
229  config defender_cfg(
230  "id", "statistics_dialog_dummy_defender",
231  "hide_help", true,
232  "do_not_list", true,
233  "hitpoints", overall_strikes
234  );
235  unit_type defender_type(defender_cfg);
236  unit_types.build_unit_type(defender_type, unit_type::BUILD_STATUS::FULL);
237 
238  battle_context_unit_stats defender_bc(&defender_type, nullptr, false, nullptr, nullptr, 0 /* not used */);
239  auto current_defender = std::make_unique<combatant>(defender_bc);
240 
241  for(const auto& i : by_cth) {
242  int cth = i.first;
243  config attacker_cfg(
244  "id", "statistics_dialog_dummy_attacker" + std::to_string(cth),
245  "hide_help", true,
246  "do_not_list", true,
247  "hitpoints", 1
248  );
249  unit_type attacker_type(attacker_cfg);
250  unit_types.build_unit_type(attacker_type, unit_type::BUILD_STATUS::FULL);
251 
252  auto attack = std::make_shared<attack_type>(config(
253  "type", "blade",
254  "range", "melee",
255  "name", "dummy attack",
256  "damage", 1,
257  "number", i.second.strikes
258  ));
259 
260  battle_context_unit_stats attacker_bc(&attacker_type, attack, true, &defender_type, nullptr, 100 - cth);
261  defender_bc = battle_context_unit_stats(&defender_type, nullptr, false, &attacker_type, attack, 0 /* not used */);
262 
263  // Update current_defender with the new defender_bc.
264  current_defender.reset(new combatant(*current_defender, defender_bc));
265 
266  combatant attacker(attacker_bc);
267  attacker.fight(*current_defender);
268  }
269 
270  const std::vector<double>& final_hp_dist = current_defender->hp_dist;
271  const auto chance_of_exactly_N_hits = [&final_hp_dist](int n) { return final_hp_dist[final_hp_dist.size() - 1 - n]; };
272 
273  // The a priori probability of scoring less hits than the actual number of hits
274  // aka "percentile" or "p-value"
275  double probability_lt = 0.0;
276  for(unsigned int i = 0; i < overall_hits; ++i) {
277  probability_lt += chance_of_exactly_N_hits(i);
278  }
279  // The a priori probability of scoring exactly the actual number of hits
280  double probability_eq = chance_of_exactly_N_hits(overall_hits);
281  // The a priori probability of scoring more hits than the actual number of hits
282  double probability_gt = 1.0 - (probability_lt + probability_eq);
283 
284  if(overall_strikes == 0) {
285  // Start of turn
286  str2 << font::unicode_em_dash;
287  } else {
288  const auto add_probability = [&str2](double probability, bool more_is_better) {
289  str2 << markup::span_color(
290  game_config::red_to_green((more_is_better ? probability : 1.0 - probability) * 100.0, true),
291  get_probability_string(probability));
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>("stats_list_hits");
313 
315  widget_item item;
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>("hits_this_turn_header");
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>("hits_this_turn_header");
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>("stats_list_main");
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>("stats_list_damage");
387 
388  damage_list.clear();
389 
390  listbox& hits_list = find_widget<listbox>("stats_list_hits");
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>("scenario_menu").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>("stats_list_main").get_selected_row();
433  if(selected_row == -1) {
434  return;
435  }
436 
437  listbox& unit_list = find_widget<listbox>("stats_list_units");
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 
448  widget_item item;
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:405
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
std::ostringstream wrapper.
Definition: formatter.hpp:40
unsigned add_row(const unsigned count=1)
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: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: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:1029
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:202
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:203
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)
Definition: markup.hpp:68
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:1500