The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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"
22#include "formatter.hpp"
24#include "gettext.hpp"
25#include "gui/widgets/label.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
37static 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
50namespace gui2::dialogs
51{
52REGISTER_DIALOG(statistics_dialog)
53
54statistics_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,
90
91 //
92 // Set up primary stats list
93 //
94 listbox& stat_list = find_widget<listbox>("stats_list_main");
95
96 connect_signal_notify_modified(stat_list,
98
100}
101
106
107void 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
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.
127static 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.
202static 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,
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,
371 show_this_turn
372 );
373 add_hits_row(_("Inflicted"), true,
374 stats.by_cth_inflicted,
376 show_this_turn
377 );
378
379 add_damage_row(_("Taken"),
380 stats.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:290
void set_values(const std::vector<::config > &values, unsigned selected=0)
virtual void set_label(const t_string &text)
const t_string & get_label() const
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)
static int sum_str_int_map(const std::map< std::string, int > &m)
This class stores all the data for a single 'side' (in game nomenclature).
Definition team.hpp:74
const std::string & side_name() const
Definition team.hpp:334
const std::string & color() const
Definition team.hpp:283
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:1253
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.
static std::string _(const char *str)
Definition gettext.hpp:100
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
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)
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::string_view data
Definition picture.cpp:188
std::unique_ptr< MIX_Audio, decltype(&MIX_DestroyAudio)> value
Definition sound.cpp:139
static std::string get_probability_string(const double prob)
Structure describing the statistics of a unit involved in the battle.
Definition attack.hpp:72
All combat-related info.
void fight(combatant &opponent, bool levelup_considered=true, bool use_biased_rng=false)
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
#define i