The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
attack_prediction.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2006 - 2025
3 by Rusty Russell <rusty@rustcorp.com.au>
4 Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5
6 This program is free software; you can redistribute it and/or modify
7 it under the terms of the GNU General Public License as published by
8 the Free Software Foundation; either version 2 of the License, or
9 (at your option) any later version.
10 This program is distributed in the hope that it will be useful,
11 but WITHOUT ANY WARRANTY.
12
13 See the COPYING file for more details.
14*/
15
16/**
17 * @file
18 * Simulate combat to calculate attacks.
19 * This can be compiled as a stand-alone program to either verify
20 * correctness or to benchmark performance.
21 *
22 * Compile with -O3 -DBENCHMARK for speed testing, and with -DCHECK for
23 * testing correctness (redirect the output to a file, then compile
24 * utils/wesnoth-attack-sim.c and run that with the arguments
25 * --check <file name>).
26 * For either option, use -DHUMAN_READABLE if you want to see the results
27 * from each combat displayed in a prettier format (but no longer suitable
28 * for wesnoth-attack-sim.c).
29 */
30
31#include "attack_prediction.hpp"
32
33#include "actions/attack.hpp"
34#include "game_config.hpp"
36#include "random.hpp"
37
38#include <array>
39#include <cfloat>
40#include <memory>
41#include <numeric>
42
43#if defined(BENCHMARK) || defined(CHECK)
44#include <chrono>
45#include <cstdio>
46#include <cstdlib>
47#endif
48
49#ifdef ATTACK_PREDICTION_DEBUG
50#define debug(x) printf x
51#else
52#define debug(x)
53#endif
54
55#ifdef ATTACK_PREDICTION_DEBUG
56namespace
57{
58/** Prints the attack statistics of a unit to cout. */
59void dump(const battle_context_unit_stats& stats)
60{
61 std::ostringstream ss;
62
63 ss << "==================================";
64 << std::boolalpha
65 << "\n" << "is_attacker: " << stats.is_attacker
66 << "\n" << "is_poisoned: " << stats.is_poisoned
67 << "\n" << "is_slowed: " << stats.is_slowed
68 << "\n" << "slows: " << stats.slows
69 << "\n" << "drains: " << stats.drains
70 << "\n" << "petrifies: " << stats.petrifies
71 << "\n" << "poisons: " << stats.poisons
72 << "\n" << "swarm: " << stats.swarm
73 << "\n" << "firststrike: " << stats.firststrike
74 << std::noboolalpha
75 << "\n" << "rounds: " << stats.rounds
76 << "\n\n"
77 << "\n" << "hp: " << stats.hp
78 << "\n" << "max_hp: " << stats.max_hp
79 << "\n" << "chance_to_hit: " << stats.chance_to_hit
80 << "\n" << "damage: " << stats.damage
81 << "\n" << "slow_damage: " << stats.slow_damage
82 << "\n" << "drain_percent: " << stats.drain_percent
83 << "\n" << "drain_constant: " << stats.drain_constant
84 << "\n" << "num_blows: " << stats.num_blows
85 << "\n" << "swarm_min: " << stats.swarm_min
86 << "\n" << "swarm_max: " << stats.swarm_max
87 << "\n\n";
88
89 std::cout << ss.rdbuf() << std::endl;
90}
91}
92#endif
93
94namespace
95{
96using summary_t = std::array<std::vector<double>, 2>;
97
98/**
99* A struct to describe one possible combat scenario.
100* (Needed when the number of attacks can vary due to swarm.)
101*/
102struct combat_slice
103{
104 // The hit point range this slice covers.
105 unsigned begin_hp; // included in the range.
106 unsigned end_hp; // excluded from the range.
107
108 // The probability of this slice.
109 double prob;
110
111 // The number of strikes applicable with this slice.
112 unsigned strikes;
113
114 combat_slice(
115 const summary_t& src_summary, unsigned begin, unsigned end, unsigned num_strikes);
116 combat_slice(const summary_t& src_summary, unsigned num_strikes);
117};
118
119/**
120* Creates a slice from a summary, and associates a number of strikes.
121*/
122combat_slice::combat_slice(
123 const summary_t& src_summary, unsigned begin, unsigned end, unsigned num_strikes)
124 : begin_hp(begin)
125 , end_hp(end)
126 , prob(0.0)
127 , strikes(num_strikes)
128{
129 if(src_summary[0].empty()) {
130 // No summary; this should be the only slice.
131 prob = 1.0;
132 return;
133 }
134
135 // Avoid accessing beyond the end of the vectors.
136 if(end > src_summary[0].size()) {
137 end = src_summary[0].size();
138 }
139
140 // Sum the probabilities in the slice.
141 for(unsigned i = begin; i < end; ++i) {
142 prob += src_summary[0][i];
143 }
144
145 if(!src_summary[1].empty()) {
146 for(unsigned i = begin; i < end; ++i) {
147 prob += src_summary[1][i];
148 }
149 }
150}
151
152/**
153* Creates a slice from the summaries, and associates a number of strikes.
154* This version of the constructor creates a slice consisting of everything.
155*/
156combat_slice::combat_slice(const summary_t& src_summary, unsigned num_strikes)
157 : begin_hp(0)
158 , end_hp(src_summary[0].size())
159 , prob(1.0)
160 , strikes(num_strikes)
161{
162}
163
164/**
165* Returns the number of hit points greater than cur_hp, and at most
166* stats.max_hp+1, at which the unit would get another attack because
167* of swarm.
168* Helper function for split_summary().
169*/
170unsigned hp_for_next_attack(unsigned cur_hp, const battle_context_unit_stats& stats)
171{
172 unsigned old_strikes = stats.calc_blows(cur_hp);
173
174 // A formula would have to deal with rounding issues; instead
175 // loop until we find more strikes.
176 while(++cur_hp <= stats.max_hp) {
177 if(stats.calc_blows(cur_hp) != old_strikes) {
178 break;
179 }
180 }
181
182 return cur_hp;
183}
184
185/**
186* Split the combat by number of attacks per combatant (for swarm).
187* This also clears the current summaries.
188*/
189std::vector<combat_slice> split_summary(
190 const battle_context_unit_stats& unit_stats, summary_t& summary)
191{
192 std::vector<combat_slice> result;
193
194 if(unit_stats.swarm_min == unit_stats.swarm_max || summary[0].empty()) {
195 // We use the same number of blows for all possibilities.
196 result.emplace_back(summary, unit_stats.num_blows);
197 return result;
198 }
199
200 debug(("Slicing:\n"));
201 // Loop through our slices.
202 unsigned cur_end = 0;
203 do {
204 // Advance to the next slice.
205 const unsigned cur_begin = cur_end;
206 cur_end = hp_for_next_attack(cur_begin, unit_stats);
207
208 // Add this slice.
209 combat_slice slice(summary, cur_begin, cur_end, unit_stats.calc_blows(cur_begin));
210 if(slice.prob != 0.0) {
211 result.push_back(slice);
212 debug(("\t%2u-%2u hp; strikes: %u; probability: %6.2f\n", cur_begin, cur_end, slice.strikes,
213 slice.prob * 100.0));
214 }
215 } while(cur_end <= unit_stats.max_hp);
216
217 return result;
218}
219
220/**
221 * A matrix of A's hitpoints vs B's hitpoints vs. their slowed states.
222 * This class is concerned only with the matrix implementation and
223 * implements functionality for shifting and retrieving probabilities
224 * (i.e. low-level stuff).
225 */
226class prob_matrix
227{
228 // Since this gets used very often (especially by the AI), it has
229 // been optimized for speed as a sparse matrix.
230public:
231 prob_matrix(unsigned int a_max,
232 unsigned int b_max,
233 bool need_a_slowed,
234 bool need_b_slowed,
235 unsigned int a_cur,
236 unsigned int b_cur,
237 const summary_t& a_initial,
238 const summary_t& b_initial);
239
240 // Shift columns on this plane (b taking damage).
241 void shift_cols(unsigned dst, unsigned src, unsigned damage, double prob, int drain_constant, int drain_percent);
242 // Shift rows on this plane (a taking damage).
243 void shift_rows(unsigned dst, unsigned src, unsigned damage, double prob, int drain_constant, int drain_percent);
244
245 /** Move a column (adding it to the destination). */
246 void move_column(unsigned d_plane, unsigned s_plane, unsigned d_col, unsigned s_col);
247 /** Move a row (adding it to the destination). */
248 void move_row(unsigned d_plane, unsigned s_plane, unsigned d_row, unsigned s_row);
249
250 // Move values within a row (or column) to a specified column (or row).
251 void merge_col(unsigned d_plane, unsigned s_plane, unsigned col, unsigned d_row);
252 void merge_cols(unsigned d_plane, unsigned s_plane, unsigned d_row);
253 void merge_row(unsigned d_plane, unsigned s_plane, unsigned row, unsigned d_col);
254 void merge_rows(unsigned d_plane, unsigned s_plane, unsigned d_col);
255
256 // Set all values to zero and clear the lists of used columns/rows.
257 void clear();
258
259 // Record the result of a single Monte Carlo simulation iteration.
260 void record_monte_carlo_result(unsigned int a_hp, unsigned int b_hp, bool a_slowed, bool b_slowed);
261
262 // Returns the index of the plane with the given slow statuses.
263 static unsigned int plane_index(bool a_slowed, bool b_slowed)
264 {
265 return (a_slowed ? 1 : 0) + (b_slowed ? 2 : 0);
266 }
267
268 /** What is the chance that an indicated combatant (one of them) is at zero? */
269 double prob_of_zero(bool check_a, bool check_b) const;
270 /** Sums the values in the specified row. */
271 double row_sum(unsigned plane, unsigned row) const;
272 /** Sums the values in the specified column. */
273 double col_sum(unsigned plane, unsigned column) const;
274 /** Sums the values in the specified plane. */
275 void sum(unsigned plane, std::vector<double>& row_sums, std::vector<double>& col_sums) const;
276
277 /** Returns true if the specified plane might have data in it. */
278 bool plane_used(unsigned p) const
279 {
280 return p < NUM_PLANES && plane_[p] != nullptr;
281 }
282
283 unsigned int num_rows() const
284 {
285 return rows_;
286 }
287 unsigned int num_cols() const
288 {
289 return cols_;
290 }
291
292 // Debugging tool.
293 void dump() const;
294
295 // We need four matrices, or "planes", reflecting the possible
296 // "slowed" states (neither slowed, A slowed, B slowed, both slowed).
297 enum {
298 NEITHER_SLOWED,
299 A_SLOWED,
300 B_SLOWED,
301 BOTH_SLOWED,
302 NUM_PLANES // Symbolic constant for the number of planes.
303 };
304
305private:
306 // This gives me 10% speed improvement over std::vector<> (g++4.0.3 x86)
307 std::unique_ptr<double[]> new_plane() const;
308
309 void initialize_plane(unsigned plane,
310 unsigned a_cur,
311 unsigned b_cur,
312 const std::vector<double>& a_initial,
313 const std::vector<double>& b_initial);
314 void initialize_row(
315 unsigned plane, unsigned row, double row_prob, unsigned b_cur, const std::vector<double>& b_initial);
316
317 double& val(unsigned plane, unsigned row, unsigned col);
318 const double& val(unsigned plane, unsigned row, unsigned col) const;
319
320 /** Transfers a portion (value * prob) of one value in the matrix to another. */
321 void xfer(unsigned dst_plane,
322 unsigned src_plane,
323 unsigned row_dst,
324 unsigned col_dst,
325 unsigned row_src,
326 unsigned col_src,
327 double prob);
328 /** Transfers one value in the matrix to another. */
329 void xfer(unsigned dst_plane,
330 unsigned src_plane,
331 unsigned row_dst,
332 unsigned col_dst,
333 unsigned row_src,
334 unsigned col_src);
335
336 void shift_cols_in_row(unsigned dst,
337 unsigned src,
338 unsigned row,
339 const std::vector<unsigned>& cols,
340 unsigned damage,
341 double prob,
342 int drainmax,
343 int drain_constant,
344 int drain_percent);
345 void shift_rows_in_col(unsigned dst,
346 unsigned src,
347 unsigned col,
348 const std::vector<unsigned>& rows,
349 unsigned damage,
350 double prob,
351 int drainmax,
352 int drain_constant,
353 int drain_percent);
354
355private: // data
356 const unsigned int rows_, cols_;
357 std::array<std::unique_ptr<double[]>, NUM_PLANES> plane_;
358
359 // For optimization, we keep track of the rows and columns with data.
360 // (The matrices are likely going to be rather sparse, with data on a grid.)
361 std::array<std::set<unsigned>, NUM_PLANES> used_rows_, used_cols_;
362};
363
364/**
365 * Constructor.
366 * @param a_max The maximum value we will track for A.
367 * @param b_max The maximum value we will track for B.
368 * @param need_a_slowed Set to true if there might be transfers to a "slow" plane for A.
369 * @param need_b_slowed Set to true if there might be transfers to a "slow" plane for B.
370 * @param a_cur The current value for A. (Ignored if a_initial[0] is not empty.)
371 * @param b_cur The current value for B. (Ignored if b_initial[0] is not empty.)
372 * @param a_initial The initial distribution of values for A. Element [0] is for normal A. while [1] is for slowed
373 * A.
374 * @param b_initial The initial distribution of values for B. Element [0] is for normal B. while [1] is for slowed
375 * B.
376 */
377prob_matrix::prob_matrix(unsigned int a_max,
378 unsigned int b_max,
379 bool need_a_slowed,
380 bool need_b_slowed,
381 unsigned int a_cur,
382 unsigned int b_cur,
383 const summary_t& a_initial,
384 const summary_t& b_initial)
385 : rows_(a_max + 1)
386 , cols_(b_max + 1)
387 , plane_()
388 , used_rows_()
389 , used_cols_()
390{
391 // Make sure we do not access the matrix in invalid positions.
392 a_cur = std::min<unsigned int>(a_cur, rows_ - 1);
393 b_cur = std::min<unsigned int>(b_cur, cols_ - 1);
394
395 // It will be convenient to always consider row/col 0 to be used.
396 for(unsigned plane = 0; plane != NUM_PLANES; ++plane) {
397 used_rows_[plane].insert(0u);
398 used_cols_[plane].insert(0u);
399 }
400
401 // We will need slowed planes if the initial vectors have them.
402 need_a_slowed = need_a_slowed || !a_initial[1].empty();
403 need_b_slowed = need_b_slowed || !b_initial[1].empty();
404
405 // Allocate the needed planes.
406 plane_[NEITHER_SLOWED] = new_plane();
407 plane_[A_SLOWED] = !need_a_slowed ? nullptr : new_plane();
408 plane_[B_SLOWED] = !need_b_slowed ? nullptr : new_plane();
409 plane_[BOTH_SLOWED] = !(need_a_slowed && need_b_slowed) ? nullptr : new_plane();
410
411 // Initialize the probability distribution.
412 initialize_plane(NEITHER_SLOWED, a_cur, b_cur, a_initial[0], b_initial[0]);
413
414 if(!a_initial[1].empty()) {
415 initialize_plane(A_SLOWED, a_cur, b_cur, a_initial[1], b_initial[0]);
416 }
417
418 if(!b_initial[1].empty()) {
419 initialize_plane(B_SLOWED, a_cur, b_cur, a_initial[0], b_initial[1]);
420 }
421
422 if(!a_initial[1].empty() && !b_initial[1].empty()) {
423 initialize_plane(BOTH_SLOWED, a_cur, b_cur, a_initial[1], b_initial[1]);
424 }
425
426 // Some debugging messages.
427 if(!a_initial[0].empty()) {
428 debug(("A has fought before (or is slowed).\n"));
429 dump();
430 }
431
432 if(!b_initial[0].empty()) {
433 debug(("B has fought before (or is slowed).\n"));
434 dump();
435 }
436}
437
438/** Allocate a new probability array, initialized to 0. */
439std::unique_ptr<double[]> prob_matrix::new_plane() const
440{
441 return std::make_unique<double[]>(std::size_t{rows_ * cols_});
442}
443
444/**
445 * Fills the indicated plane with its initial (non-zero) values.
446 * (Part of construction)
447 * @param plane The plane to initialize.
448 * @param a_cur The current value for A. (Ignored if a_initial is not empty.)
449 * @param b_cur The current value for B. (Ignored if b_initial is not empty.)
450 * @param a_initial The initial distribution of values for A for this plane.
451 * @param b_initial The initial distribution of values for B for this plane.
452 */
453void prob_matrix::initialize_plane(unsigned plane,
454 unsigned a_cur,
455 unsigned b_cur,
456 const std::vector<double>& a_initial,
457 const std::vector<double>& b_initial)
458{
459 if(!a_initial.empty()) {
460 unsigned row_count = std::min<unsigned>(a_initial.size(), rows_);
461 // The probabilities for each row are contained in a_initial.
462 for(unsigned row = 0; row < row_count; ++row) {
463 if(a_initial[row] != 0.0) {
464 used_rows_[plane].insert(row);
465 initialize_row(plane, row, a_initial[row], b_cur, b_initial);
466 }
467 }
468 } else {
469 used_rows_[plane].insert(a_cur);
470 // Only the row indicated by a_cur is a possibility.
471 initialize_row(plane, a_cur, 1.0, b_cur, b_initial);
472 }
473}
474
475/**
476 * Fills the indicated row with its initial (non-zero) values.
477 * (Part of construction)
478 * @param plane The plane containing the row to initialize.
479 * @param row The row to initialize.
480 * @param row_prob The probability of A having the value for this row.
481 * @param b_cur The current value for B. (Ignored if b_initial is not empty.)
482 * @param b_initial The initial distribution of values for B for this plane.
483 */
484void prob_matrix::initialize_row(
485 unsigned plane, unsigned row, double row_prob, unsigned b_cur, const std::vector<double>& b_initial)
486{
487 if(!b_initial.empty()) {
488 unsigned col_count = std::min<unsigned>(b_initial.size(), cols_);
489 // The probabilities for each column are contained in b_initial.
490 for(unsigned col = 0; col < col_count; ++col) {
491 if(b_initial[col] != 0.0) {
492 used_cols_[plane].insert(col);
493 val(plane, row, col) = row_prob * b_initial[col];
494 }
495 }
496 } else {
497 // Only the column indicated by b_cur is a possibility.
498 used_cols_[plane].insert(b_cur);
499 val(plane, row, b_cur) = row_prob;
500 }
501}
502
503double& prob_matrix::val(unsigned plane, unsigned row, unsigned col)
504{
505 assert(row < rows_);
506 assert(col < cols_);
507 return plane_[plane][row * cols_ + col];
508}
509
510const double& prob_matrix::val(unsigned plane, unsigned row, unsigned col) const
511{
512 assert(row < rows_);
513 assert(col < cols_);
514 return plane_[plane][row * cols_ + col];
515}
516
517// xfer, shift_cols and shift_rows use up most of our time. Careful!
518/**
519 * Transfers a portion (value * prob) of one value in the matrix to another.
520 */
521void prob_matrix::xfer(unsigned dst_plane,
522 unsigned src_plane,
523 unsigned row_dst,
524 unsigned col_dst,
525 unsigned row_src,
526 unsigned col_src,
527 double prob)
528{
529 double& src = val(src_plane, row_src, col_src);
530 if(src != 0.0) {
531 double diff = src * prob;
532 src -= diff;
533
534 double& dst = val(dst_plane, row_dst, col_dst);
535 if(dst == 0.0) {
536 // Track that this entry is now used.
537 used_rows_[dst_plane].insert(row_dst);
538 used_cols_[dst_plane].insert(col_dst);
539 }
540 dst += diff;
541
542 debug(("Shifted %4.3g from %s(%u,%u) to %s(%u,%u).\n", diff,
543 src_plane == NEITHER_SLOWED
544 ? ""
545 : src_plane == A_SLOWED
546 ? "[A_SLOWED]"
547 : src_plane == B_SLOWED
548 ? "[B_SLOWED]"
549 : src_plane == BOTH_SLOWED
550 ? "[BOTH_SLOWED]"
551 : "INVALID",
552
553 row_src, col_src,
554 dst_plane == NEITHER_SLOWED
555 ? ""
556 : dst_plane == A_SLOWED
557 ? "[A_SLOWED]"
558 : dst_plane == B_SLOWED
559 ? "[B_SLOWED]"
560 : dst_plane == BOTH_SLOWED
561 ? "[BOTH_SLOWED]"
562 : "INVALID",
563 row_dst, col_dst)
564 );
565 }
566}
567
568/**
569 * Transfers one value in the matrix to another.
570 */
571void prob_matrix::xfer(
572 unsigned dst_plane, unsigned src_plane, unsigned row_dst, unsigned col_dst, unsigned row_src, unsigned col_src)
573{
574 if(dst_plane == src_plane && row_dst == row_src && col_dst == col_src)
575 // Transferring to itself. Nothing to do.
576 return;
577
578 double& src = val(src_plane, row_src, col_src);
579 if(src != 0.0) {
580 debug(("Shifting %4.3g from %s(%u,%u) to %s(%u,%u).\n", src,
581 src_plane == NEITHER_SLOWED
582 ? ""
583 : src_plane == A_SLOWED
584 ? "[A_SLOWED]"
585 : src_plane == B_SLOWED
586 ? "[B_SLOWED]"
587 : src_plane == BOTH_SLOWED
588 ? "[BOTH_SLOWED]"
589 : "INVALID",
590 row_src, col_src,
591 dst_plane == NEITHER_SLOWED
592 ? ""
593 : dst_plane == A_SLOWED
594 ? "[A_SLOWED]"
595 : dst_plane == B_SLOWED
596 ? "[B_SLOWED]"
597 : dst_plane == BOTH_SLOWED
598 ? "[BOTH_SLOWED]"
599 : "INVALID",
600 row_dst, col_dst)
601 );
602
603 double& dst = val(dst_plane, row_dst, col_dst);
604 if(dst == 0.0) {
605 // Track that this entry is now used.
606 used_rows_[dst_plane].insert(row_dst);
607 used_cols_[dst_plane].insert(col_dst);
608 }
609
610 dst += src;
611 src = 0.0;
612 }
613}
614
615/**
616 * Transfers a portion (value * prob) of the values in a row to another.
617 * Part of shift_cols().
618 */
619void prob_matrix::shift_cols_in_row(unsigned dst,
620 unsigned src,
621 unsigned row,
622 const std::vector<unsigned>& cols,
623 unsigned damage,
624 double prob,
625 int drainmax,
626 int drain_constant,
627 int drain_percent)
628{
629 // Some conversions to (signed) int.
630 int row_i = static_cast<int>(row);
631 int max_row = static_cast<int>(rows_) - 1;
632
633 // cols[0] is excluded since that should be 0, representing already dead.
634 unsigned col_x = 1;
635
636 // Killing blows can have different drain amounts, so handle them first
637 for(; col_x < cols.size() && cols[col_x] < damage; ++col_x) {
638 // These variables are not strictly necessary, but they make the
639 // calculation easier to parse.
640 int col_i = static_cast<int>(cols[col_x]);
641 int drain_amount = col_i * drain_percent / 100 + drain_constant;
642 unsigned newrow = std::clamp(row_i + drain_amount, 1, max_row);
643 xfer(dst, src, newrow, 0, row, cols[col_x], prob);
644 }
645
646 // The remaining columns use the specified drainmax.
647 unsigned newrow = std::clamp(row_i + drainmax, 1, max_row);
648 for(; col_x < cols.size(); ++col_x) {
649 xfer(dst, src, newrow, cols[col_x] - damage, row, cols[col_x], prob);
650 }
651}
652
653/**
654 * Transfers a portion (value * prob) of each column in a plane to another.
655 * Each column in the @a src plane gets shifted @a damage columns towards 0, and
656 * also shifted into the @a dst plane. In addition, the rows can shift if
657 * @a drain constant or @a drain_percent is nonzero.
658 */
659void prob_matrix::shift_cols(
660 unsigned dst, unsigned src, unsigned damage, double prob, int drain_constant, int drain_percent)
661{
662 int drainmax = (drain_percent * (static_cast<signed>(damage)) / 100 + drain_constant);
663
664 if(drain_constant || drain_percent) {
665 debug(("Drains %i (%i%% of %u plus %i)\n", drainmax, drain_percent, damage, drain_constant));
666 }
667
668 // Get lists of indices currently used in the source plane.
669 // (This needs to be cached since we might add indices while shifting.)
670 const std::vector<unsigned> rows(used_rows_[src].begin(), used_rows_[src].end());
671 const std::vector<unsigned> cols(used_cols_[src].begin(), used_cols_[src].end());
672
673 // Loop downwards if we drain positive, but upwards if we drain negative,
674 // so we write behind us (for when src == dst).
675 if(drainmax > 0) {
676 // rows[0] is excluded since that should be 0, representing already dead.
677 for(unsigned row_x = rows.size() - 1; row_x != 0; --row_x) {
678 shift_cols_in_row(dst, src, rows[row_x], cols, damage, prob, drainmax, drain_constant, drain_percent);
679 }
680 } else {
681 // rows[0] is excluded since that should be 0, representing already dead.
682 for(unsigned row_x = 1; row_x != rows.size(); ++row_x) {
683 shift_cols_in_row(dst, src, rows[row_x], cols, damage, prob, drainmax, drain_constant, drain_percent);
684 }
685 }
686}
687
688/**
689 * Transfers a portion (value * prob) of the values in a column to another.
690 * Part of shift_rows().
691 */
692void prob_matrix::shift_rows_in_col(unsigned dst,
693 unsigned src,
694 unsigned col,
695 const std::vector<unsigned>& rows,
696 unsigned damage,
697 double prob,
698 int drainmax,
699 int drain_constant,
700 int drain_percent)
701{
702 // Some conversions to (signed) int.
703 int col_i = static_cast<int>(col);
704 int max_col = static_cast<int>(cols_) - 1;
705
706 // rows[0] is excluded since that should be 0, representing already dead.
707 unsigned row_x = 1;
708
709 // Killing blows can have different drain amounts, so handle them first
710 for(; row_x < rows.size() && rows[row_x] < damage; ++row_x) {
711 // These variables are not strictly necessary, but they make the
712 // calculation easier to parse.
713 int row_i = static_cast<int>(rows[row_x]);
714 int drain_amount = row_i * drain_percent / 100 + drain_constant;
715 unsigned newcol = std::clamp(col_i + drain_amount, 1, max_col);
716 xfer(dst, src, 0, newcol, rows[row_x], col, prob);
717 }
718
719 // The remaining rows use the specified drainmax.
720 unsigned newcol = std::clamp(col_i + drainmax, 1, max_col);
721 for(; row_x < rows.size(); ++row_x) {
722 xfer(dst, src, rows[row_x] - damage, newcol, rows[row_x], col, prob);
723 }
724}
725
726/**
727 * Transfers a portion (value * prob) of each row in a plane to another.
728 * Each row in the @a src plane gets shifted @a damage columns towards 0, and
729 * also shifted into the @a dst plane. In addition, the columns can shift if
730 * @a drain constant or @a drain_percent is nonzero.
731 */
732void prob_matrix::shift_rows(
733 unsigned dst, unsigned src, unsigned damage, double prob, int drain_constant, int drain_percent)
734{
735 int drainmax = (drain_percent * (static_cast<signed>(damage)) / 100 + drain_constant);
736
737 if(drain_constant || drain_percent) {
738 debug(("Drains %i (%i%% of %u plus %i)\n", drainmax, drain_percent, damage, drain_constant));
739 }
740
741 // Get lists of indices currently used in the source plane.
742 // (This needs to be cached since we might add indices while shifting.)
743 const std::vector<unsigned> rows(used_rows_[src].begin(), used_rows_[src].end());
744 const std::vector<unsigned> cols(used_cols_[src].begin(), used_cols_[src].end());
745
746 // Loop downwards if we drain positive, but upwards if we drain negative,
747 // so we write behind us (for when src == dst).
748 if(drainmax > 0) {
749 // cols[0] is excluded since that should be 0, representing already dead.
750 for(unsigned col_x = cols.size() - 1; col_x != 0; --col_x)
751 shift_rows_in_col(dst, src, cols[col_x], rows, damage, prob, drainmax, drain_constant, drain_percent);
752 } else {
753 // cols[0] is excluded since that should be 0, representing already dead.
754 for(unsigned col_x = 1; col_x != cols.size(); ++col_x) {
755 shift_rows_in_col(dst, src, cols[col_x], rows, damage, prob, drainmax, drain_constant, drain_percent);
756 }
757 }
758}
759
760/**
761 * Move a column (adding it to the destination).
762 */
763void prob_matrix::move_column(unsigned d_plane, unsigned s_plane, unsigned d_col, unsigned s_col)
764{
765 // Transfer the data.
766 for(const unsigned& row : used_rows_[s_plane]) {
767 xfer(d_plane, s_plane, row, d_col, row, s_col);
768 }
769}
770
771/**
772 * Move a row (adding it to the destination).
773 */
774void prob_matrix::move_row(unsigned d_plane, unsigned s_plane, unsigned d_row, unsigned s_row)
775{
776 // Transfer the data.
777 for(const unsigned& col : used_cols_[s_plane]) {
778 xfer(d_plane, s_plane, d_row, col, s_row, col);
779 }
780}
781
782/**
783 * Move values in the specified column -- excluding row zero -- to the
784 * specified row in that column (possibly shifting planes in the process).
785 */
786void prob_matrix::merge_col(unsigned d_plane, unsigned s_plane, unsigned col, unsigned d_row)
787{
788 auto rows_end = used_rows_[s_plane].end();
789 auto row_it = used_rows_[s_plane].begin();
790
791 // Transfer the data, excluding row zero.
792 for(++row_it; row_it != rows_end; ++row_it) {
793 xfer(d_plane, s_plane, d_row, col, *row_it, col);
794 }
795}
796
797/**
798 * Move values within columns in the specified plane -- excluding row zero --
799 * to the specified row (possibly shifting planes in the process).
800 */
801void prob_matrix::merge_cols(unsigned d_plane, unsigned s_plane, unsigned d_row)
802{
803 auto rows_end = used_rows_[s_plane].end();
804 auto row_it = used_rows_[s_plane].begin();
805
806 // Transfer the data, excluding row zero.
807 for(++row_it; row_it != rows_end; ++row_it) {
808 for(const unsigned& col : used_cols_[s_plane]) {
809 xfer(d_plane, s_plane, d_row, col, *row_it, col);
810 }
811 }
812}
813
814/**
815 * Move values in the specified row -- excluding column zero -- to the
816 * specified column in that row (possibly shifting planes in the process).
817 */
818void prob_matrix::merge_row(unsigned d_plane, unsigned s_plane, unsigned row, unsigned d_col)
819{
820 auto cols_end = used_cols_[s_plane].end();
821 auto col_it = used_cols_[s_plane].begin();
822
823 // Transfer the data, excluding column zero.
824 for(++col_it; col_it != cols_end; ++col_it) {
825 xfer(d_plane, s_plane, row, d_col, row, *col_it);
826 }
827}
828
829/**
830 * Move values within rows in the specified plane -- excluding column zero --
831 * to the specified column (possibly shifting planes in the process).
832 */
833void prob_matrix::merge_rows(unsigned d_plane, unsigned s_plane, unsigned d_col)
834{
835 auto cols_end = used_cols_[s_plane].end();
836 // Exclude column zero.
837 auto cols_begin = std::next(used_cols_[s_plane].begin());
838
839 // Transfer the data, excluding column zero.
840 for(const unsigned row : used_rows_[s_plane]) {
841 for(auto col_it = cols_begin; col_it != cols_end; ++col_it) {
842 xfer(d_plane, s_plane, row, d_col, row, *col_it);
843 }
844 }
845}
846
847/**
848 * Set all values to zero and clear the lists of used columns/rows.
849 */
850void prob_matrix::clear()
851{
852 for(unsigned int p = 0u; p < NUM_PLANES; ++p) {
853 if(!plane_used(p)) {
854 continue;
855 }
856
857 if(used_rows_[p].empty()) {
858 // Nothing to do
859 continue;
860 }
861
862 auto [first_row, last_row] = std::minmax_element(used_rows_[p].begin(), used_rows_[p].end());
863 for(unsigned int r = *first_row; r <= *last_row; ++r) {
864 for(unsigned int c = 0u; c < cols_; ++c) {
865 plane_[p][r * cols_ + c] = 0.0;
866 }
867 }
868
869 used_rows_[p].clear();
870 used_cols_[p].clear();
871
872 /* Row and column 0 are always considered to be used.
873 Functions like merge_col() access *used_rows_[plane].begin() without checking if there are
874 any used rows: thus, ensuring that there are used rows and columns is necessary to avoid
875 memory corruption. */
876 used_rows_[p].insert(0u);
877 used_cols_[p].insert(0u);
878 }
879}
880
881/**
882 * Record the result of a single Monte Carlo simulation iteration.
883 */
884void prob_matrix::record_monte_carlo_result(unsigned int a_hp, unsigned int b_hp, bool a_slowed, bool b_slowed)
885{
886 assert(a_hp <= rows_);
887 assert(b_hp <= cols_);
888 unsigned int plane = plane_index(a_slowed, b_slowed);
889 ++val(plane, a_hp, b_hp);
890 used_rows_[plane].insert(a_hp);
891 used_cols_[plane].insert(b_hp);
892}
893
894/**
895 * What is the chance that an indicated combatant (one of them) is at zero?
896 */
897double prob_matrix::prob_of_zero(bool check_a, bool check_b) const
898{
899 double prob = 0.0;
900
901 for(unsigned p = 0; p < NUM_PLANES; ++p) {
902 if(!plane_used(p)) {
903 continue;
904 }
905
906 // Column 0 is where b is at zero.
907 if(check_b) {
908 for(const unsigned& row : used_rows_[p]) {
909 prob += val(p, row, 0);
910 }
911 }
912
913 // Row 0 is where a is at zero.
914 if(check_a) {
915 for(const unsigned& col : used_cols_[p]) {
916 prob += val(p, 0, col);
917 }
918 }
919 // Theoretically, if checking both, we should subtract the chance that
920 // both are dead, but that chance is zero, so don't worry about it.
921 }
922
923 return prob;
924}
925
926/**
927 * Sums the values in the specified row.
928 */
929double prob_matrix::row_sum(unsigned plane, unsigned row) const
930{
931 if(!plane_used(plane)) {
932 return 0.0;
933 }
934
935 double sum = 0.0;
936 for(unsigned col : used_cols_[plane]) {
937 sum += val(plane, row, col);
938 }
939 return sum;
940}
941
942/**
943 * Sums the values in the specified column.
944 */
945double prob_matrix::col_sum(unsigned plane, unsigned column) const
946{
947 if(!plane_used(plane)) {
948 return 0.0;
949 }
950
951 double sum = 0.0;
952 for(unsigned row : used_rows_[plane]) {
953 sum += val(plane, row, column);
954 }
955 return sum;
956}
957
958/**
959 * Sums the values in the specified plane.
960 * The sum of each row is added to the corresponding entry in row_sums.
961 * The sum of each column is added to the corresponding entry in col_sums.
962 */
963void prob_matrix::sum(unsigned plane, std::vector<double>& row_sums, std::vector<double>& col_sums) const
964{
965 for(const unsigned& row : used_rows_[plane]) {
966 for(const unsigned& col : used_cols_[plane]) {
967 const double& prob = val(plane, row, col);
968 row_sums[row] += prob;
969 col_sums[col] += prob;
970 }
971 }
972}
973
974#if defined(CHECK) && defined(ATTACK_PREDICTION_DEBUG)
975void prob_matrix::dump() const
976{
977 unsigned int row, col, m;
978 const char* names[] {"NEITHER_SLOWED", "A_SLOWED", "B_SLOWED", "BOTH_SLOWED"};
979
980 for(m = 0; m < NUM_PLANES; ++m) {
981 if(!plane_used(m)) {
982 continue;
983 }
984
985 debug(("%s:\n", names[m]));
986 for(row = 0; row < rows_; ++row) {
987 debug((" "));
988 for(col = 0; col < cols_; ++col) {
989 debug(("%4.3g ", val(m, row, col) * 100));
990 }
991
992 debug(("\n"));
993 }
994 }
995}
996#else
997void prob_matrix::dump() const
998{
999}
1000#endif
1001
1002/**
1003 * A matrix for calculating the outcome of combat.
1004 * This class specifies the interface and functionality shared between
1005 * probability_combat_matrix and monte_carlo_combat_matrix.
1006 */
1007class combat_matrix : protected prob_matrix
1008{
1009public:
1010 combat_matrix(unsigned int a_max_hp,
1011 unsigned int b_max_hp,
1012 unsigned int a_hp,
1013 unsigned int b_hp,
1014 const summary_t& a_summary,
1015 const summary_t& b_summary,
1016 bool a_slows,
1017 bool b_slows,
1018 unsigned int a_damage,
1019 unsigned int b_damage,
1020 unsigned int a_slow_damage,
1021 unsigned int b_slow_damage,
1022 int a_drain_percent,
1023 int b_drain_percent,
1024 int a_drain_constant,
1025 int b_drain_constant);
1026
1027 virtual ~combat_matrix()
1028 {
1029 }
1030
1031 // We lied: actually did less damage, adjust matrix.
1032 void remove_petrify_distortion_a(unsigned damage, unsigned slow_damage, unsigned b_hp);
1033 void remove_petrify_distortion_b(unsigned damage, unsigned slow_damage, unsigned a_hp);
1034
1035 void forced_levelup_a();
1036 void conditional_levelup_a();
1037
1038 void forced_levelup_b();
1039 void conditional_levelup_b();
1040
1041 using prob_matrix::row_sum;
1042 using prob_matrix::col_sum;
1043
1044 // Its over, and here's the bill.
1045 virtual void extract_results(
1046 summary_t& summary_a, summary_t& summary_b)
1047 = 0;
1048
1049 void dump() const
1050 {
1051 prob_matrix::dump();
1052 }
1053
1054protected:
1055 unsigned a_max_hp_;
1056 bool a_slows_;
1057 unsigned a_damage_;
1058 unsigned a_slow_damage_;
1059 int a_drain_percent_;
1060 int a_drain_constant_;
1061
1062 unsigned b_max_hp_;
1063 bool b_slows_;
1064 unsigned b_damage_;
1065 unsigned b_slow_damage_;
1066 int b_drain_percent_;
1067 int b_drain_constant_;
1068};
1069
1070/**
1071 * Constructor.
1072 * @param a_max_hp The maximum hit points for A.
1073 * @param b_max_hp The maximum hit points for B.
1074 * @param a_slows Set to true if A slows B when A hits B.
1075 * @param b_slows Set to true if B slows A when B hits A.
1076 * @param a_hp The current hit points for A. (Ignored if a_summary[0] is not empty.)
1077 * @param b_hp The current hit points for B. (Ignored if b_summary[0] is not empty.)
1078 * @param a_summary The hit point distribution for A (from previous combats). Element [0] is for normal A. while
1079 * [1] is for slowed A.
1080 * @param b_summary The hit point distribution for B (from previous combats). Element [0] is for normal B. while
1081 * [1] is for slowed B.
1082 */
1083
1084combat_matrix::combat_matrix(unsigned int a_max_hp,
1085 unsigned int b_max_hp,
1086 unsigned int a_hp,
1087 unsigned int b_hp,
1088 const summary_t& a_summary,
1089 const summary_t& b_summary,
1090 bool a_slows,
1091 bool b_slows,
1092 unsigned int a_damage,
1093 unsigned int b_damage,
1094 unsigned int a_slow_damage,
1095 unsigned int b_slow_damage,
1096 int a_drain_percent,
1097 int b_drain_percent,
1098 int a_drain_constant,
1099 int b_drain_constant)
1100 // The inversion of the order of the *_slows parameters here is intentional.
1101 : prob_matrix(a_max_hp, b_max_hp, b_slows, a_slows, a_hp, b_hp, a_summary, b_summary)
1102 , a_max_hp_(a_max_hp)
1103 , a_slows_(a_slows)
1104 , a_damage_(a_damage)
1105 , a_slow_damage_(a_slow_damage)
1106 , a_drain_percent_(a_drain_percent)
1107 , a_drain_constant_(a_drain_constant)
1108 , b_max_hp_(b_max_hp)
1109 , b_slows_(b_slows)
1110 , b_damage_(b_damage)
1111 , b_slow_damage_(b_slow_damage)
1112 , b_drain_percent_(b_drain_percent)
1113 , b_drain_constant_(b_drain_constant)
1114{
1115}
1116
1117// We lied: actually did less damage, adjust matrix.
1118void combat_matrix::remove_petrify_distortion_a(unsigned damage, unsigned slow_damage, unsigned b_hp)
1119{
1120 for(int p = 0; p < NUM_PLANES; ++p) {
1121 if(!plane_used(p)) {
1122 continue;
1123 }
1124
1125 // A is slow in planes 1 and 3.
1126 unsigned actual_damage = (p & 1) ? slow_damage : damage;
1127 if(b_hp > actual_damage) {
1128 // B was actually petrified, not killed.
1129 move_column(p, p, b_hp - actual_damage, 0);
1130 }
1131 }
1132}
1133
1134void combat_matrix::remove_petrify_distortion_b(unsigned damage, unsigned slow_damage, unsigned a_hp)
1135{
1136 for(int p = 0; p < NUM_PLANES; ++p) {
1137 if(!plane_used(p)) {
1138 continue;
1139 }
1140
1141 // B is slow in planes 2 and 3.
1142 unsigned actual_damage = (p & 2) ? slow_damage : damage;
1143 if(a_hp > actual_damage) {
1144 // A was actually petrified, not killed.
1145 move_row(p, p, a_hp - actual_damage, 0);
1146 }
1147 }
1148}
1149
1150void combat_matrix::forced_levelup_a()
1151{
1152 /* Move all the values (except 0hp) of all the planes to the "fully healed"
1153 row of the planes unslowed for A. */
1154 for(int p = 0; p < NUM_PLANES; ++p) {
1155 if(plane_used(p)) {
1156 merge_cols(p & -2, p, a_max_hp_);
1157 }
1158 }
1159}
1160
1161void combat_matrix::forced_levelup_b()
1162{
1163 /* Move all the values (except 0hp) of all the planes to the "fully healed"
1164 column of planes unslowed for B. */
1165 for(int p = 0; p < NUM_PLANES; ++p) {
1166 if(plane_used(p)) {
1167 merge_rows(p & -3, p, b_max_hp_);
1168 }
1169 }
1170}
1171
1172void combat_matrix::conditional_levelup_a()
1173{
1174 /* Move the values of the first column (except 0hp) of all the
1175 planes to the "fully healed" row of the planes unslowed for A. */
1176 for(int p = 0; p < NUM_PLANES; ++p) {
1177 if(plane_used(p)) {
1178 merge_col(p & -2, p, 0, a_max_hp_);
1179 }
1180 }
1181}
1182
1183void combat_matrix::conditional_levelup_b()
1184{
1185 /* Move the values of the first row (except 0hp) of all the
1186 planes to the last column of the planes unslowed for B. */
1187 for(int p = 0; p < NUM_PLANES; ++p) {
1188 if(plane_used(p)) {
1189 merge_row(p & -3, p, 0, b_max_hp_);
1190 }
1191 }
1192}
1193
1194/**
1195 * Implementation of combat_matrix that calculates exact probabilities of events.
1196 * Fast in "simple" fights (low number of strikes, low HP, and preferably no slow
1197 * or swarm effect), but can be unusably expensive in extremely complex situations.
1198 */
1199class probability_combat_matrix : public combat_matrix
1200{
1201public:
1202 probability_combat_matrix(unsigned int a_max_hp,
1203 unsigned int b_max_hp,
1204 unsigned int a_hp,
1205 unsigned int b_hp,
1206 const summary_t& a_summary,
1207 const summary_t& b_summary,
1208 bool a_slows,
1209 bool b_slows,
1210 unsigned int a_damage,
1211 unsigned int b_damage,
1212 unsigned int a_slow_damage,
1213 unsigned int b_slow_damage,
1214 int a_drain_percent,
1215 int b_drain_percent,
1216 int a_drain_constant,
1217 int b_drain_constant);
1218
1219 // A hits B.
1220 void receive_blow_b(double hit_chance);
1221 // B hits A. Why can't they just get along?
1222 void receive_blow_a(double hit_chance);
1223
1224 /** What is the chance that one of the combatants is dead? */
1225 double dead_prob() const
1226 {
1227 return prob_of_zero(true, true);
1228 }
1229
1230 /** What is the chance that combatant 'a' is dead? */
1231 double dead_prob_a() const
1232 {
1233 return prob_of_zero(true, false);
1234 }
1235
1236 /** What is the chance that combatant 'b' is dead? */
1237 double dead_prob_b() const
1238 {
1239 return prob_of_zero(false, true);
1240 }
1241
1242 void extract_results(
1243 summary_t& summary_a, summary_t& summary_b) override;
1244};
1245
1246/**
1247 * Constructor.
1248 * @param a_max_hp The maximum hit points for A.
1249 * @param b_max_hp The maximum hit points for B.
1250 * @param a_slows Set to true if A slows B when A hits B.
1251 * @param b_slows Set to true if B slows A when B hits A.
1252 * @param a_hp The current hit points for A. (Ignored if a_summary[0] is not empty.)
1253 * @param b_hp The current hit points for B. (Ignored if b_summary[0] is not empty.)
1254 * @param a_summary The hit point distribution for A (from previous combats). Element [0] is for normal A. while
1255 * [1] is for slowed A.
1256 * @param b_summary The hit point distribution for B (from previous combats). Element [0] is for normal B. while
1257 * [1] is for slowed B.
1258 */
1259probability_combat_matrix::probability_combat_matrix(unsigned int a_max_hp,
1260 unsigned int b_max_hp,
1261 unsigned int a_hp,
1262 unsigned int b_hp,
1263 const summary_t& a_summary,
1264 const summary_t& b_summary,
1265 bool a_slows,
1266 bool b_slows,
1267 unsigned int a_damage,
1268 unsigned int b_damage,
1269 unsigned int a_slow_damage,
1270 unsigned int b_slow_damage,
1271 int a_drain_percent,
1272 int b_drain_percent,
1273 int a_drain_constant,
1274 int b_drain_constant)
1275 : combat_matrix(a_max_hp,
1276 b_max_hp,
1277 a_hp,
1278 b_hp,
1279 a_summary,
1280 b_summary,
1281 a_slows,
1282 b_slows,
1283 a_damage,
1284 b_damage,
1285 a_slow_damage,
1286 b_slow_damage,
1287 a_drain_percent,
1288 b_drain_percent,
1289 a_drain_constant,
1290 b_drain_constant)
1291{
1292}
1293
1294// Shift combat_matrix to reflect the probability 'hit_chance' that damage
1295// is done to 'b'.
1296void probability_combat_matrix::receive_blow_b(double hit_chance)
1297{
1298 // Walk backwards so we don't copy already-copied matrix planes.
1299 unsigned src = NUM_PLANES;
1300 while(src-- != 0) {
1301 if(!plane_used(src)) {
1302 continue;
1303 }
1304
1305 // If A slows us, we go from 0=>2, 1=>3, 2=>2 3=>3.
1306 int dst = a_slows_ ? src | 2 : src;
1307
1308 // A is slow in planes 1 and 3.
1309 unsigned damage = (src & 1) ? a_slow_damage_ : a_damage_;
1310
1311 shift_cols(dst, src, damage, hit_chance, a_drain_constant_, a_drain_percent_);
1312 }
1313}
1314
1315// Shift matrix to reflect probability 'hit_chance'
1316// that damage (up to) 'damage' is done to 'a'.
1317void probability_combat_matrix::receive_blow_a(double hit_chance)
1318{
1319 // Walk backwards so we don't copy already-copied matrix planes.
1320 unsigned src = NUM_PLANES;
1321 while(src-- != 0) {
1322 if(!plane_used(src)) {
1323 continue;
1324 }
1325
1326 // If B slows us, we go from 0=>1, 1=>1, 2=>3 3=>3.
1327 int dst = b_slows_ ? src | 1 : src;
1328
1329 // B is slow in planes 2 and 3.
1330 unsigned damage = (src & 2) ? b_slow_damage_ : b_damage_;
1331
1332 shift_rows(dst, src, damage, hit_chance, b_drain_constant_, b_drain_percent_);
1333 }
1334}
1335
1336void probability_combat_matrix::extract_results(
1337 summary_t& summary_a, summary_t& summary_b)
1338{
1339 // Reset the summaries.
1340 summary_a[0] = std::vector<double>(num_rows());
1341 summary_b[0] = std::vector<double>(num_cols());
1342
1343 if(plane_used(A_SLOWED)) {
1344 summary_a[1] = std::vector<double>(num_rows());
1345 }
1346
1347 if(plane_used(B_SLOWED)) {
1348 summary_b[1] = std::vector<double>(num_cols());
1349 }
1350
1351 for(unsigned p = 0; p < NUM_PLANES; ++p) {
1352 if(!plane_used(p)) {
1353 continue;
1354 }
1355
1356 // A is slow in planes 1 and 3.
1357 unsigned dst_a = (p & 1) ? 1u : 0u;
1358 // B is slow in planes 2 and 3.
1359 unsigned dst_b = (p & 2) ? 1u : 0u;
1360 sum(p, summary_a[dst_a], summary_b[dst_b]);
1361 }
1362}
1363
1364/**
1365 * Implementation of combat_matrix based on Monte Carlo simulation.
1366 * This does not give exact results, but the error should be small
1367 * thanks to the law of large numbers. Probably more important is that
1368 * the simulation time doesn't depend on anything other than the number
1369 * of strikes, which makes this method much faster if the combatants
1370 * have a lot of HP.
1371 */
1372class monte_carlo_combat_matrix : public combat_matrix
1373{
1374public:
1375 monte_carlo_combat_matrix(unsigned int a_max_hp,
1376 unsigned int b_max_hp,
1377 unsigned int a_hp,
1378 unsigned int b_hp,
1379 const summary_t& a_summary,
1380 const summary_t& b_summary,
1381 bool a_slows,
1382 bool b_slows,
1383 unsigned int a_damage,
1384 unsigned int b_damage,
1385 unsigned int a_slow_damage,
1386 unsigned int b_slow_damage,
1387 int a_drain_percent,
1388 int b_drain_percent,
1389 int a_drain_constant,
1390 int b_drain_constant,
1391 unsigned int rounds,
1392 unsigned int a_chance_to_hit,
1393 unsigned int b_chance_to_hit,
1394 const std::vector<combat_slice>& a_split,
1395 const std::vector<combat_slice>& b_split,
1396 double a_initially_slowed_chance,
1397 double b_initially_slowed_chance,
1398 bool use_biased_rng);
1399
1400 void simulate();
1401
1402 void extract_results(
1403 summary_t& summary_a, summary_t& summary_b) override;
1404
1405 double get_a_hit_probability() const;
1406 double get_b_hit_probability() const;
1407
1408private:
1409 static const unsigned int NUM_ITERATIONS = 5000u;
1410
1411 std::vector<double> a_initial_;
1412 std::vector<double> b_initial_;
1413 std::vector<double> a_initial_slowed_;
1414 std::vector<double> b_initial_slowed_;
1415 std::vector<combat_slice> a_split_;
1416 std::vector<combat_slice> b_split_;
1417 unsigned int rounds_;
1418 double a_hit_chance_;
1419 double b_hit_chance_;
1420 unsigned int a_chance_to_hit_;
1421 unsigned int b_chance_to_hit_;
1422 bool use_biased_rng_;
1423 double a_initially_slowed_chance_;
1424 double b_initially_slowed_chance_;
1425 unsigned int iterations_a_hit_ = 0u;
1426 unsigned int iterations_b_hit_ = 0u;
1427
1428 unsigned int calc_blows_a(unsigned int a_hp) const;
1429 unsigned int calc_blows_b(unsigned int b_hp) const;
1430 static void divide_all_elements(std::vector<double>& vec, double divisor);
1431 static void scale_probabilities(
1432 const std::vector<double>& source, std::vector<double>& target, double divisor, unsigned int singular_hp);
1433};
1434
1435monte_carlo_combat_matrix::monte_carlo_combat_matrix(unsigned int a_max_hp,
1436 unsigned int b_max_hp,
1437 unsigned int a_hp,
1438 unsigned int b_hp,
1439 const summary_t& a_summary,
1440 const summary_t& b_summary,
1441 bool a_slows,
1442 bool b_slows,
1443 unsigned int a_damage,
1444 unsigned int b_damage,
1445 unsigned int a_slow_damage,
1446 unsigned int b_slow_damage,
1447 int a_drain_percent,
1448 int b_drain_percent,
1449 int a_drain_constant,
1450 int b_drain_constant,
1451 unsigned int rounds,
1452 unsigned int a_chance_to_hit,
1453 unsigned int b_chance_to_hit,
1454 const std::vector<combat_slice>& a_split,
1455 const std::vector<combat_slice>& b_split,
1456 double a_initially_slowed_chance,
1457 double b_initially_slowed_chance,
1458 bool use_biased_rng)
1459 : combat_matrix(a_max_hp,
1460 b_max_hp,
1461 a_hp,
1462 b_hp,
1463 a_summary,
1464 b_summary,
1465 a_slows,
1466 b_slows,
1467 a_damage,
1468 b_damage,
1469 a_slow_damage,
1470 b_slow_damage,
1471 a_drain_percent,
1472 b_drain_percent,
1473 a_drain_constant,
1474 b_drain_constant)
1475 , a_split_(a_split)
1476 , b_split_(b_split)
1477 , rounds_(rounds)
1478 , a_hit_chance_(a_chance_to_hit / 100.0)
1479 , b_hit_chance_(b_chance_to_hit / 100.0)
1480 , a_chance_to_hit_(a_chance_to_hit)
1481 , b_chance_to_hit_(b_chance_to_hit)
1482 , use_biased_rng_(use_biased_rng)
1483 , a_initially_slowed_chance_(a_initially_slowed_chance)
1484 , b_initially_slowed_chance_(b_initially_slowed_chance)
1485{
1486 scale_probabilities(a_summary[0], a_initial_, 1.0 - a_initially_slowed_chance, a_hp);
1487 scale_probabilities(a_summary[1], a_initial_slowed_, a_initially_slowed_chance, a_hp);
1488 scale_probabilities(b_summary[0], b_initial_, 1.0 - b_initially_slowed_chance, b_hp);
1489 scale_probabilities(b_summary[1], b_initial_slowed_, b_initially_slowed_chance, b_hp);
1490
1491 clear();
1492}
1493
1494void monte_carlo_combat_matrix::simulate()
1495{
1497
1498 for(unsigned int i = 0u; i < NUM_ITERATIONS; ++i) {
1499 bool a_hit = false;
1500 bool b_hit = false;
1501 bool a_slowed = rng.get_random_bool(a_initially_slowed_chance_);
1502 bool b_slowed = rng.get_random_bool(b_initially_slowed_chance_);
1503 const std::vector<double>& a_initial = a_slowed ? a_initial_slowed_ : a_initial_;
1504 const std::vector<double>& b_initial = b_slowed ? b_initial_slowed_ : b_initial_;
1505 // In the a_initial vector, a_initial[x] is the chance of having exactly x hp.
1506 // Thus the index returned by get_random_element corresponds directly to an amount of hp.
1507 auto a_hp = static_cast<unsigned int>(rng.get_random_element(a_initial.begin(), a_initial.end()));
1508 auto b_hp = static_cast<unsigned int>(rng.get_random_element(b_initial.begin(), b_initial.end()));
1509 unsigned int a_strikes = calc_blows_a(a_hp);
1510 unsigned int b_strikes = calc_blows_b(b_hp);
1511
1512 int a_credit = use_biased_rng_ ? biased_rng::roll_credit(rng) : 0;
1513 int b_credit = use_biased_rng_ ? biased_rng::roll_credit(rng) : 0;
1514
1515 for(unsigned int j = 0u; j < rounds_ && a_hp > 0u && b_hp > 0u; ++j) {
1516 for(unsigned int k = 0u; k < std::max(a_strikes, b_strikes); ++k) {
1517 if(k < a_strikes) {
1518 const bool a_hits = use_biased_rng_
1519 ? biased_rng::roll_hit(rng, a_strikes - k, a_chance_to_hit_, a_credit)
1520 : rng.get_random_bool(a_hit_chance_);
1521 if(a_hits) {
1522 // A hits B
1523 unsigned int damage = a_slowed ? a_slow_damage_ : a_damage_;
1524 damage = std::min(damage, b_hp);
1525 b_hit = true;
1526 b_slowed |= a_slows_;
1527
1528 int drain_amount = (a_drain_percent_ * static_cast<signed>(damage) / 100 + a_drain_constant_);
1529 a_hp = std::clamp(a_hp + drain_amount, 1u, a_max_hp_);
1530
1531 b_hp -= damage;
1532
1533 if(b_hp == 0u) {
1534 // A killed B
1535 break;
1536 }
1537 }
1538 }
1539
1540 if(k < b_strikes) {
1541 const bool b_hits = use_biased_rng_
1542 ? biased_rng::roll_hit(rng, b_strikes - k, b_chance_to_hit_, b_credit)
1543 : rng.get_random_bool(b_hit_chance_);
1544 if(b_hits) {
1545 // B hits A
1546 unsigned int damage = b_slowed ? b_slow_damage_ : b_damage_;
1547 damage = std::min(damage, a_hp);
1548 a_hit = true;
1549 a_slowed |= b_slows_;
1550
1551 int drain_amount = (b_drain_percent_ * static_cast<signed>(damage) / 100 + b_drain_constant_);
1552 b_hp = std::clamp(b_hp + drain_amount, 1u, b_max_hp_);
1553
1554 a_hp -= damage;
1555
1556 if(a_hp == 0u) {
1557 // B killed A
1558 break;
1559 }
1560 }
1561 }
1562 }
1563 }
1564
1565 iterations_a_hit_ += a_hit ? 1 : 0;
1566 iterations_b_hit_ += b_hit ? 1 : 0;
1567
1568 record_monte_carlo_result(a_hp, b_hp, a_slowed, b_slowed);
1569 }
1570}
1571
1572/**
1573 * Otherwise the same as in probability_combat_matrix, but this needs to divide the values
1574 * by the number of iterations.
1575 */
1576void monte_carlo_combat_matrix::extract_results(
1577 summary_t& summary_a, summary_t& summary_b)
1578{
1579 // Reset the summaries.
1580 summary_a[0] = std::vector<double>(num_rows());
1581 summary_b[0] = std::vector<double>(num_cols());
1582
1583 if(plane_used(A_SLOWED)) {
1584 summary_a[1] = std::vector<double>(num_rows());
1585 }
1586
1587 if(plane_used(B_SLOWED)) {
1588 summary_b[1] = std::vector<double>(num_cols());
1589 }
1590
1591 for(unsigned p = 0; p < NUM_PLANES; ++p) {
1592 if(!plane_used(p)) {
1593 continue;
1594 }
1595
1596 // A is slow in planes 1 and 3.
1597 unsigned dst_a = (p & 1) ? 1u : 0u;
1598 // B is slow in planes 2 and 3.
1599 unsigned dst_b = (p & 2) ? 1u : 0u;
1600 sum(p, summary_a[dst_a], summary_b[dst_b]);
1601 }
1602
1603 divide_all_elements(summary_a[0], static_cast<double>(NUM_ITERATIONS));
1604 divide_all_elements(summary_b[0], static_cast<double>(NUM_ITERATIONS));
1605
1606 if(plane_used(A_SLOWED)) {
1607 divide_all_elements(summary_a[1], static_cast<double>(NUM_ITERATIONS));
1608 }
1609
1610 if(plane_used(B_SLOWED)) {
1611 divide_all_elements(summary_b[1], static_cast<double>(NUM_ITERATIONS));
1612 }
1613}
1614
1615double monte_carlo_combat_matrix::get_a_hit_probability() const
1616{
1617 return static_cast<double>(iterations_a_hit_) / static_cast<double>(NUM_ITERATIONS);
1618}
1619
1620double monte_carlo_combat_matrix::get_b_hit_probability() const
1621{
1622 return static_cast<double>(iterations_b_hit_) / static_cast<double>(NUM_ITERATIONS);
1623}
1624
1625unsigned int monte_carlo_combat_matrix::calc_blows_a(unsigned int a_hp) const
1626{
1627 auto it = a_split_.begin();
1628 while(it != a_split_.end() && it->end_hp <= a_hp) {
1629 ++it;
1630 }
1631
1632 if(it == a_split_.end()) {
1633 --it;
1634 }
1635
1636 return it->strikes;
1637}
1638
1639unsigned int monte_carlo_combat_matrix::calc_blows_b(unsigned int b_hp) const
1640{
1641 auto it = b_split_.begin();
1642 while(it != b_split_.end() && it->end_hp <= b_hp) {
1643 ++it;
1644 }
1645
1646 if(it == b_split_.end()) {
1647 --it;
1648 }
1649
1650 return it->strikes;
1651}
1652
1653void monte_carlo_combat_matrix::scale_probabilities(
1654 const std::vector<double>& source, std::vector<double>& target, double divisor, unsigned int singular_hp)
1655{
1656 if(divisor == 0.0) {
1657 // Happens if the "target" HP distribution vector isn't used,
1658 // in which case it's not necessary to scale the probabilities.
1659 return;
1660 }
1661
1662 if(source.empty()) {
1663 target.resize(singular_hp + 1u, 0.0);
1664 target[singular_hp] = 1.0;
1665 } else {
1666 std::transform(
1667 source.begin(), source.end(), std::back_inserter(target), [=](double prob) { return prob / divisor; });
1668 }
1669
1670 assert(std::abs(std::accumulate(target.begin(), target.end(), 0.0) - 1.0) < 0.001);
1671}
1672
1673void monte_carlo_combat_matrix::divide_all_elements(std::vector<double>& vec, double divisor)
1674{
1675 for(double& e : vec) {
1676 e /= divisor;
1677 }
1678}
1679
1680} // end anon namespace
1681
1683 : hp_dist(u.max_hp + 1, 0.0)
1684 , untouched(0.0)
1685 , poisoned(0.0)
1686 , slowed(0.0)
1687 , u_(u)
1688{
1689 // We inherit current state from previous combatant.
1690 if(prev) {
1691 summary[0] = prev->summary[0];
1692 summary[1] = prev->summary[1];
1693 hp_dist = prev->hp_dist;
1694 untouched = prev->untouched;
1695 poisoned = prev->poisoned;
1696 slowed = prev->slowed;
1697 } else {
1698 hp_dist[std::min(u.hp, u.max_hp)] = 1.0;
1699 untouched = 1.0;
1700 poisoned = u.is_poisoned ? 1.0 : 0.0;
1701 slowed = u.is_slowed ? 1.0 : 0.0;
1702
1703 // If we're already slowed, create summary[1] so that probability calculation code
1704 // knows that we're slowed.
1705 if(u.is_slowed) {
1706 summary[0].resize(u.max_hp + 1, 0.0);
1707 summary[1] = hp_dist;
1708 }
1709 }
1710}
1711
1712// Copy constructor (except use this copy of battle_context_unit_stats)
1714 : hp_dist(that.hp_dist)
1715 , untouched(that.untouched)
1716 , poisoned(that.poisoned)
1717 , slowed(that.slowed)
1718 , u_(u)
1719{
1720 summary[0] = that.summary[0];
1721 summary[1] = that.summary[1];
1722}
1723
1724namespace
1725{
1726enum class attack_prediction_mode { probability_calculation, monte_carlo_simulation, biased_rng_simulation };
1727
1728void forced_levelup(std::vector<double>& hp_dist)
1729{
1730 /* If we survive the combat, we will level up. So the probability
1731 of death is unchanged, but all other cases get merged into the
1732 fully healed case. */
1733 auto i = hp_dist.begin();
1734 // Skip to the second value.
1735 for(++i; i != hp_dist.end(); ++i) {
1736 *i = 0;
1737 }
1738
1739 // Full heal unless dead.
1740 hp_dist.back() = 1 - hp_dist.front();
1741}
1742
1743void conditional_levelup(std::vector<double>& hp_dist, double kill_prob)
1744{
1745 /* If we kill, we will level up. So then the damage we had becomes
1746 less probable since it's now conditional on us not leveling up.
1747 This doesn't apply to the probability of us dying, of course. */
1748 double scalefactor = 0;
1749 const double chance_to_survive = 1 - hp_dist.front();
1750 if(chance_to_survive > DBL_MIN) {
1751 scalefactor = 1 - kill_prob / chance_to_survive;
1752 }
1753
1754 auto i = hp_dist.begin();
1755 // Skip to the second value.
1756 for(++i; i != hp_dist.end(); ++i) {
1757 *i *= scalefactor;
1758 }
1759
1760 // Full heal if leveled up.
1761 hp_dist.back() += kill_prob;
1762}
1763
1764/* Calculates the probability that we will be poisoned after the fight. Parameters:
1765 * initial_prob: how likely we are to be poisoned before the fight.
1766 * enemy_gives: true if the enemy poisons us.
1767 * prob_touched: probability the enemy touches us.
1768 * prob_stay_alive: probability we survive the fight alive.
1769 * kill_heals: true if killing the enemy heals the poison (in other words, we get a level-up).
1770 * prob_kill: probability we kill the enemy.
1771 */
1772double calculate_probability_of_debuff(double initial_prob, bool enemy_gives, double prob_touched, double prob_stay_alive, bool kill_heals, double prob_kill)
1773{
1774 assert(initial_prob >= 0.0 && initial_prob <= 1.0);
1775 /* In the add-on "Legend of the Invincibles", the "distant attack" ability sets the probability of being touched to zero.
1776 With some optimizing compilers, the probability can somehow get slightly negative, see bug #2342.
1777 Ensure that it gets non-negative. */
1778 prob_touched = std::max(prob_touched, 0.0);
1779 // Prob_stay_alive can get slightly negative because of a rounding error, so ensure that it gets non-negative.
1780 prob_stay_alive = std::max(prob_stay_alive, 0.0);
1781 // Prob_kill can creep a bit above 100 % if the AI simulates an unit being attacked by multiple units in a row, due to rounding error.
1782 // Likewise, it can get slightly negative if the unit already has negative HP.
1783 // Simply limit it to suitable range.
1784 prob_kill = std::clamp(prob_kill, 0.0, 1.0);
1785
1786 // Probability we are already debuffed and the enemy doesn't hit us.
1787 const double prob_already_debuffed_not_touched = initial_prob * (1.0 - prob_touched);
1788 // Probability we are already debuffed and the enemy hits us.
1789 const double prob_already_debuffed_touched = initial_prob * prob_touched;
1790 // Probability we aren't debuffed and the enemy doesn't hit us.
1791 // const double prob_initially_healthy_not_touched = (1.0 - initial_prob) * (1.0 - prob_touched);
1792 // Probability we aren't debuffed and the enemy hits us.
1793 const double prob_initially_healthy_touched = (1.0 - initial_prob) * prob_touched;
1794
1795 // Probability to survive if the enemy doesn't hit us.
1796 const double prob_survive_if_not_hit = 1.0;
1797 // Probability to survive if the enemy hits us.
1798 const double prob_survive_if_hit = prob_touched > 0.0 ? (prob_stay_alive - (1.0 - prob_touched)) / prob_touched : 1.0;
1799
1800 // Probability to kill if we don't survive the fight.
1801 // const double prob_kill_if_not_survive = 0.0;
1802 // Probability to kill if we survive the fight.
1803 const double prob_kill_if_survive = prob_stay_alive > 0.0 ? prob_kill / prob_stay_alive : 0.0;
1804
1805 // Probability to be debuffed after the fight, calculated in multiple stages.
1806 double prob_debuff = 0.0;
1807
1808 if(!kill_heals) {
1809 prob_debuff += prob_already_debuffed_not_touched;
1810 } else {
1811 prob_debuff += prob_already_debuffed_not_touched * (1.0 - prob_survive_if_not_hit * prob_kill_if_survive);
1812 }
1813
1814 if(!kill_heals) {
1815 prob_debuff += prob_already_debuffed_touched;
1816 } else {
1817 prob_debuff += prob_already_debuffed_touched * (1.0 - prob_survive_if_hit * prob_kill_if_survive);
1818 }
1819
1820 // "Originally not debuffed, not hit" situation never results in us getting debuffed. Skipping.
1821
1822 if(!enemy_gives) {
1823 // Do nothing.
1824 } else if(!kill_heals) {
1825 prob_debuff += prob_initially_healthy_touched;
1826 } else {
1827 prob_debuff += prob_initially_healthy_touched * (1.0 - prob_survive_if_hit * prob_kill_if_survive);
1828 }
1829
1830 return prob_debuff;
1831}
1832
1833// Rounds a probability that's extremely close to 0 or 1 to exactly 0 or 1.
1834void round_prob_if_close_to_sure(double& prob)
1835{
1836 if(prob < 1.0e-9) {
1837 prob = 0.0;
1838 } else if(prob > 1.0 - 1.0e-9) {
1839 prob = 1.0;
1840 }
1841}
1842
1843/**
1844 * Returns the smallest HP we could possibly have based on the provided
1845 * hit point distribution.
1846 */
1847unsigned min_hp(const std::vector<double>& hp_dist, unsigned def)
1848{
1849 const unsigned size = hp_dist.size();
1850
1851 // Look for a nonzero entry.
1852 for(unsigned i = 0; i != size; ++i) {
1853 if(hp_dist[i] != 0.0) {
1854 return i;
1855 }
1856 }
1857
1858 // Either the distribution is empty or is full of zeros, so
1859 // return the default.
1860 return def;
1861}
1862
1863/**
1864 * Returns a number that approximates the complexity of the fight,
1865 * for the purpose of determining if it's faster to calculate exact
1866 * probabilities or to run a Monte Carlo simulation.
1867 * Ignores the numbers of rounds and strikes because these slow down
1868 * both calculation modes.
1869 */
1870unsigned int fight_complexity(unsigned int num_slices,
1871 unsigned int opp_num_slices,
1872 const battle_context_unit_stats& stats,
1873 const battle_context_unit_stats& opp_stats)
1874{
1875 return num_slices * opp_num_slices * ((stats.slows || opp_stats.is_slowed) ? 2 : 1)
1876 * ((opp_stats.slows || stats.is_slowed) ? 2 : 1) * stats.max_hp * opp_stats.max_hp;
1877}
1878
1879/**
1880 * Returns the plane index for the slow/no-slow state of the combatants.
1881 */
1882unsigned int plane_index(const battle_context_unit_stats& stats,
1883 const battle_context_unit_stats& opp_stats)
1884{
1885 return (stats.is_slowed ? 1 : 0) | (opp_stats.is_slowed ? 2 : 0);
1886}
1887
1888// Combat without chance of death, berserk, slow or drain is simple.
1889void no_death_fight(const battle_context_unit_stats& stats,
1890 const battle_context_unit_stats& opp_stats,
1891 unsigned strikes,
1892 unsigned opp_strikes,
1893 std::vector<double>& hp_dist,
1894 std::vector<double>& opp_hp_dist,
1895 double& self_not_hit,
1896 double& opp_not_hit,
1897 bool levelup_considered)
1898{
1899 // Our strikes.
1900 // If we were killed in an earlier fight, we don't get to attack.
1901 // (Most likely case: we are a first striking defender subject to a series
1902 // of attacks.)
1903 const double alive_prob = hp_dist.empty() ? 1.0 : 1.0 - hp_dist[0];
1904 const double hit_chance = (stats.chance_to_hit / 100.0) * alive_prob;
1905
1906 if(opp_hp_dist.empty()) {
1907 // Starts with a known HP, so Pascal's triangle.
1908 opp_hp_dist = std::vector<double>(opp_stats.max_hp + 1);
1909 opp_hp_dist[opp_stats.hp] = 1.0;
1910
1911 for(unsigned int i = 0; i < strikes; ++i) {
1912 for(int j = i; j >= 0; j--) {
1913 unsigned src_index = opp_stats.hp - j * stats.damage;
1914 double move = opp_hp_dist[src_index] * hit_chance;
1915 opp_hp_dist[src_index] -= move;
1916 opp_hp_dist[src_index - stats.damage] += move;
1917 }
1918
1919 opp_not_hit *= 1.0 - hit_chance;
1920 }
1921 } else {
1922 // HP could be spread anywhere, iterate through whole thing.
1923 for(unsigned int i = 0; i < strikes; ++i) {
1924 for(unsigned int j = stats.damage; j < opp_hp_dist.size(); ++j) {
1925 double move = opp_hp_dist[j] * hit_chance;
1926 opp_hp_dist[j] -= move;
1927 opp_hp_dist[j - stats.damage] += move;
1928 }
1929 opp_not_hit *= 1.0 - hit_chance;
1930 }
1931 }
1932
1933 // Opponent's strikes
1934 // If opponent was killed in an earlier fight, they don't get to attack.
1935 const double opp_alive_prob = opp_hp_dist.empty() ? 1.0 : 1.0 - opp_hp_dist[0];
1936 const double opp_hit_chance = (opp_stats.chance_to_hit / 100.0) * opp_alive_prob;
1937
1938 if(hp_dist.empty()) {
1939 // Starts with a known HP, so Pascal's triangle.
1940 hp_dist = std::vector<double>(stats.max_hp + 1);
1941 hp_dist[stats.hp] = 1.0;
1942 for(unsigned int i = 0; i < opp_strikes; ++i) {
1943 for(int j = i; j >= 0; j--) {
1944 unsigned src_index = stats.hp - j * opp_stats.damage;
1945 double move = hp_dist[src_index] * opp_hit_chance;
1946 hp_dist[src_index] -= move;
1947 hp_dist[src_index - opp_stats.damage] += move;
1948 }
1949
1950 self_not_hit *= 1.0 - opp_hit_chance;
1951 }
1952 } else {
1953 // HP could be spread anywhere, iterate through whole thing.
1954 for(unsigned int i = 0; i < opp_strikes; ++i) {
1955 for(unsigned int j = opp_stats.damage; j < hp_dist.size(); ++j) {
1956 double move = hp_dist[j] * opp_hit_chance;
1957 hp_dist[j] -= move;
1958 hp_dist[j - opp_stats.damage] += move;
1959 }
1960
1961 self_not_hit *= 1.0 - opp_hit_chance;
1962 }
1963 }
1964
1965 if(!levelup_considered || !stats.can_advance) { // we assume that the unit full-heals if it advances
1966 return;
1967 }
1968
1969 if(stats.experience + opp_stats.level >= stats.max_experience) {
1970 forced_levelup(hp_dist);
1971 }
1972
1973 if(opp_stats.experience + stats.level >= opp_stats.max_experience) {
1974 forced_levelup(opp_hp_dist);
1975 }
1976}
1977
1978// Combat with <= 1 strike each is simple, too.
1979void one_strike_fight(const battle_context_unit_stats& stats,
1980 const battle_context_unit_stats& opp_stats,
1981 unsigned strikes,
1982 unsigned opp_strikes,
1983 std::vector<double>& hp_dist,
1984 std::vector<double>& opp_hp_dist,
1985 double& self_not_hit,
1986 double& opp_not_hit,
1987 bool levelup_considered)
1988{
1989 // If we were killed in an earlier fight, we don't get to attack.
1990 // (Most likely case: we are a first striking defender subject to a series
1991 // of attacks.)
1992 double alive_prob = hp_dist.empty() ? 1.0 : 1.0 - hp_dist[0];
1993 if(stats.hp == 0) {
1994 alive_prob = 0.0;
1995 }
1996 const double hit_chance = (stats.chance_to_hit / 100.0) * alive_prob;
1997
1998 if(opp_hp_dist.empty()) {
1999 opp_hp_dist = std::vector<double>(opp_stats.max_hp + 1);
2000 if(strikes == 1 && opp_stats.hp > 0) {
2001 opp_hp_dist[opp_stats.hp] = 1.0 - hit_chance;
2002 opp_hp_dist[std::max<int>(opp_stats.hp - stats.damage, 0)] = hit_chance;
2003 opp_not_hit *= 1.0 - hit_chance;
2004 } else {
2005 opp_hp_dist[opp_stats.hp] = 1.0;
2006 }
2007 } else {
2008 if(strikes == 1) {
2009 for(unsigned int i = 1; i < opp_hp_dist.size(); ++i) {
2010 double move = opp_hp_dist[i] * hit_chance;
2011 opp_hp_dist[i] -= move;
2012 opp_hp_dist[std::max<int>(i - stats.damage, 0)] += move;
2013 }
2014
2015 opp_not_hit *= 1.0 - hit_chance;
2016 }
2017 }
2018
2019 // If we killed opponent, it won't attack us.
2020 const double opp_attack_prob = (1.0 - opp_hp_dist[0]) * alive_prob;
2021 const double opp_hit_chance = (opp_stats.chance_to_hit / 100.0) * opp_attack_prob;
2022
2023 if(hp_dist.empty()) {
2024 hp_dist = std::vector<double>(stats.max_hp + 1);
2025 if(opp_strikes == 1 && stats.hp > 0) {
2026 hp_dist[stats.hp] = 1.0 - opp_hit_chance;
2027 hp_dist[std::max<int>(stats.hp - opp_stats.damage, 0)] = opp_hit_chance;
2028 self_not_hit *= 1.0 - opp_hit_chance;
2029 } else {
2030 hp_dist[stats.hp] = 1.0;
2031 }
2032 } else {
2033 if(opp_strikes == 1) {
2034 for(unsigned int i = 1; i < hp_dist.size(); ++i) {
2035 double move = hp_dist[i] * opp_hit_chance;
2036 hp_dist[i] -= move;
2037 hp_dist[std::max<int>(i - opp_stats.damage, 0)] += move;
2038 }
2039
2040 self_not_hit *= 1.0 - opp_hit_chance;
2041 }
2042 }
2043
2044 if(!levelup_considered || !stats.can_advance) { // we assume that the unit full-heals if it advances
2045 return;
2046 }
2047
2048 if(stats.experience + game_config::combat_xp(opp_stats.level) >= stats.max_experience) {
2049 forced_levelup(hp_dist);
2050 } else if(stats.experience + game_config::kill_xp(opp_stats.level) >= stats.max_experience) {
2051 conditional_levelup(hp_dist, opp_hp_dist[0]);
2052 }
2053
2054 if(opp_stats.experience + game_config::combat_xp(stats.level) >= opp_stats.max_experience) {
2055 forced_levelup(opp_hp_dist);
2056 } else if(opp_stats.experience + game_config::kill_xp(stats.level) >= opp_stats.max_experience) {
2057 conditional_levelup(opp_hp_dist, hp_dist[0]);
2058 }
2059}
2060
2061/* The parameters "split", "opp_split", "initially_slowed_chance" and
2062"opp_initially_slowed_chance" are ignored in the probability calculation mode. */
2063void complex_fight(attack_prediction_mode mode,
2064 const battle_context_unit_stats& stats,
2065 const battle_context_unit_stats& opp_stats,
2066 unsigned strikes,
2067 unsigned opp_strikes,
2068 summary_t& summary,
2069 summary_t& opp_summary,
2070 double& self_not_hit,
2071 double& opp_not_hit,
2072 bool levelup_considered,
2073 const std::vector<combat_slice>& split,
2074 const std::vector<combat_slice>& opp_split,
2075 double initially_slowed_chance,
2076 double opp_initially_slowed_chance)
2077{
2078 unsigned int rounds = std::max<unsigned int>(stats.rounds, opp_stats.rounds);
2079 unsigned max_attacks = std::max(strikes, opp_strikes);
2080
2081 debug(("A gets %u attacks, B %u.\n", strikes, opp_strikes));
2082
2083 unsigned int a_damage = stats.damage, a_slow_damage = stats.slow_damage;
2084 unsigned int b_damage = opp_stats.damage, b_slow_damage = opp_stats.slow_damage;
2085
2086 // To simulate stoning, we set to amount which kills, and re-adjust after.
2087 /** @todo FIXME: This doesn't work for rolling calculations, just first battle.
2088 It also does not work if combined with (percentage) drain. */
2089 if(stats.petrifies) {
2090 a_damage = a_slow_damage = opp_stats.max_hp;
2091 }
2092
2093 if(opp_stats.petrifies) {
2094 b_damage = b_slow_damage = stats.max_hp;
2095 }
2096
2097 const double original_self_not_hit = self_not_hit;
2098 const double original_opp_not_hit = opp_not_hit;
2099 const double hit_chance = stats.chance_to_hit / 100.0;
2100 const double opp_hit_chance = opp_stats.chance_to_hit / 100.0;
2101 double self_hit = 0.0;
2102 double opp_hit = 0.0;
2103 double self_hit_unknown = 1.0; // Probability we don't yet know if A will be hit or not
2104 double opp_hit_unknown = 1.0; // Ditto for B
2105
2106 // Prepare the matrix that will do our calculations.
2107 std::unique_ptr<combat_matrix> matrix;
2108
2109 if(mode == attack_prediction_mode::probability_calculation) {
2110 debug(("Using exact probability calculations.\n"));
2111
2112 auto pm = std::make_unique<probability_combat_matrix>(
2113 stats.max_hp,
2114 opp_stats.max_hp,
2115 stats.hp,
2116 opp_stats.hp,
2117 summary,
2118 opp_summary,
2119 stats.slows,
2120 opp_stats.slows,
2121 a_damage,
2122 b_damage,
2123 a_slow_damage,
2124 b_slow_damage,
2125 stats.drain_percent,
2126 opp_stats.drain_percent,
2127 stats.drain_constant,
2128 opp_stats.drain_constant
2129 );
2130
2131 do {
2132 for(unsigned int i = 0; i < max_attacks; ++i) {
2133 if(i < strikes) {
2134 debug(("A strikes\n"));
2135 double b_already_dead = pm->dead_prob_b();
2136 pm->receive_blow_b(hit_chance);
2137 pm->dump();
2138
2139 double first_hit = hit_chance * opp_hit_unknown;
2140 opp_hit += first_hit;
2141 opp_hit_unknown -= first_hit;
2142 double both_were_alive = 1.0 - b_already_dead - pm->dead_prob_a();
2143 double this_hit_killed_b = both_were_alive != 0.0 ? (pm->dead_prob_b() - b_already_dead) / both_were_alive : 1.0;
2144 self_hit_unknown *= (1.0 - this_hit_killed_b);
2145 }
2146 if(i < opp_strikes) {
2147 debug(("B strikes\n"));
2148 double a_already_dead = pm->dead_prob_a();
2149 pm->receive_blow_a(opp_hit_chance);
2150 pm->dump();
2151
2152 double first_hit = opp_hit_chance * self_hit_unknown;
2153 self_hit += first_hit;
2154 self_hit_unknown -= first_hit;
2155 double both_were_alive = 1.0 - a_already_dead - pm->dead_prob_b();
2156 double this_hit_killed_a = both_were_alive != 0.0 ? (pm->dead_prob_a() - a_already_dead) / both_were_alive : 1.0;
2157 opp_hit_unknown *= (1.0 - this_hit_killed_a);
2158 }
2159 }
2160
2161 debug(("Combat ends:\n"));
2162 pm->dump();
2163 } while(--rounds && pm->dead_prob() < 0.99);
2164
2165 self_hit = std::min(self_hit, 1.0);
2166 opp_hit = std::min(opp_hit, 1.0);
2167
2168 self_not_hit = original_self_not_hit * (1.0 - self_hit);
2169 opp_not_hit = original_opp_not_hit * (1.0 - opp_hit);
2170
2171 if(stats.slows) {
2172 /* The calculation method for the "not hit" probability above is incorrect if either unit can slow.
2173 * Because a slowed unit deals less damage, it is more likely for the slowing unit to be alive if it
2174 * has hit the other unit. In that situation, the "not hit" probability can no longer be calculated
2175 * with simple addition.
2176 * Instead, just fetch the probability from the combat matrix.
2177 */
2178 unsigned int plane = plane_index(stats, opp_stats);
2179 double not_hit = pm->col_sum(plane, opp_stats.hp) + ((plane & 1) ? 0.0 : pm->col_sum(plane | 1, opp_stats.hp));
2180 opp_not_hit = original_opp_not_hit * not_hit;
2181 }
2182 if(opp_stats.slows) {
2183 unsigned int plane = plane_index(stats, opp_stats);
2184 double not_hit = pm->row_sum(plane, stats.hp) + ((plane & 2) ? 0.0 : pm->row_sum(plane | 2, stats.hp));
2185 self_not_hit = original_self_not_hit * not_hit;
2186 }
2187
2188 // Pass ownership back to the containing block
2189 matrix = std::move(pm);
2190 } else {
2191 debug(("Using Monte Carlo simulation.\n"));
2192
2193 auto mcm = std::make_unique<monte_carlo_combat_matrix>(
2194 stats.max_hp,
2195 opp_stats.max_hp,
2196 stats.hp,
2197 opp_stats.hp,
2198 summary,
2199 opp_summary,
2200 stats.slows,
2201 opp_stats.slows,
2202 a_damage,
2203 b_damage,
2204 a_slow_damage,
2205 b_slow_damage,
2206 stats.drain_percent,
2207 opp_stats.drain_percent,
2208 stats.drain_constant,
2209 opp_stats.drain_constant,
2210 rounds,
2211 stats.chance_to_hit,
2212 opp_stats.chance_to_hit,
2213 split,
2214 opp_split,
2215 initially_slowed_chance,
2216 opp_initially_slowed_chance,
2217 mode == attack_prediction_mode::biased_rng_simulation
2218 );
2219
2220 mcm->simulate();
2221 debug(("Combat ends:\n"));
2222 mcm->dump();
2223
2224 self_not_hit = 1.0 - mcm->get_a_hit_probability();
2225 opp_not_hit = 1.0 - mcm->get_b_hit_probability();
2226
2227 // Pass ownership back to the containing block
2228 matrix = std::move(mcm);
2229 }
2230
2231 if(stats.petrifies) {
2232 matrix->remove_petrify_distortion_a(stats.damage, stats.slow_damage, opp_stats.hp);
2233 }
2234
2235 if(opp_stats.petrifies) {
2236 matrix->remove_petrify_distortion_b(opp_stats.damage, opp_stats.slow_damage, stats.hp);
2237 }
2238
2239 if(levelup_considered && stats.can_advance) { // we assume that the unit full-heals if it advances
2240 if(stats.experience + game_config::combat_xp(opp_stats.level) >= stats.max_experience) {
2241 matrix->forced_levelup_a();
2242 } else if(stats.experience + game_config::kill_xp(opp_stats.level) >= stats.max_experience) {
2243 matrix->conditional_levelup_a();
2244 }
2245
2246 if(opp_stats.experience + game_config::combat_xp(stats.level) >= opp_stats.max_experience) {
2247 matrix->forced_levelup_b();
2248 } else if(opp_stats.experience + game_config::kill_xp(stats.level) >= opp_stats.max_experience) {
2249 matrix->conditional_levelup_b();
2250 }
2251 }
2252
2253 // We extract results separately, then combine.
2254 matrix->extract_results(summary, opp_summary);
2255}
2256
2257/**
2258 * Chooses the best of the various known combat calculations for the current
2259 * situation.
2260 */
2261void do_fight(const battle_context_unit_stats& stats,
2262 const battle_context_unit_stats& opp_stats,
2263 unsigned strikes,
2264 unsigned opp_strikes,
2265 summary_t& summary,
2266 summary_t& opp_summary,
2267 double& self_not_hit,
2268 double& opp_not_hit,
2269 bool levelup_considered)
2270{
2271 // Optimization only works in the simple cases (no slow, no drain,
2272 // no petrify, no berserk, and no slowed results from an earlier combat).
2273 if(!stats.slows && !opp_stats.slows && !stats.drains && !opp_stats.drains && !stats.petrifies
2274 && !opp_stats.petrifies && stats.rounds == 1 && opp_stats.rounds == 1 && summary[1].empty()
2275 && opp_summary[1].empty())
2276 {
2277 if(strikes <= 1 && opp_strikes <= 1) {
2278 one_strike_fight(stats, opp_stats, strikes, opp_strikes, summary[0], opp_summary[0], self_not_hit,
2279 opp_not_hit, levelup_considered);
2280 } else if(strikes * stats.damage < min_hp(opp_summary[0], opp_stats.hp)
2281 && opp_strikes * opp_stats.damage < min_hp(summary[0], stats.hp)) {
2282 no_death_fight(stats, opp_stats, strikes, opp_strikes, summary[0], opp_summary[0], self_not_hit,
2283 opp_not_hit, levelup_considered);
2284 } else {
2285 complex_fight(attack_prediction_mode::probability_calculation, stats, opp_stats, strikes, opp_strikes,
2286 summary, opp_summary, self_not_hit, opp_not_hit, levelup_considered, std::vector<combat_slice>(),
2287 std::vector<combat_slice>(), 0.0, 0.0);
2288 }
2289 } else {
2290 complex_fight(attack_prediction_mode::probability_calculation, stats, opp_stats, strikes, opp_strikes, summary,
2291 opp_summary, self_not_hit, opp_not_hit, levelup_considered, std::vector<combat_slice>(),
2292 std::vector<combat_slice>(), 0.0, 0.0);
2293 }
2294}
2295
2296/**
2297 * Initializes a hit point summary (assumed empty) based on the source.
2298 * Only the part of the source from begin_hp up to (not including) end_hp
2299 * is kept, and all values get scaled by prob.
2300 */
2301void init_slice_summary(
2302 std::vector<double>& dst, const std::vector<double>& src, unsigned begin_hp, unsigned end_hp, double prob)
2303{
2304 if(src.empty()) {
2305 // Nothing to do.
2306 return;
2307 }
2308
2309 const unsigned size = src.size();
2310 // Avoid going over the end of the vector.
2311 if(end_hp > size) {
2312 end_hp = size;
2313 }
2314
2315 // Initialize the destination.
2316 dst.resize(size, 0.0);
2317 for(unsigned i = begin_hp; i < end_hp; ++i) {
2318 dst[i] = src[i] / prob;
2319 }
2320}
2321
2322/**
2323 * Merges the summary results of simulation into an overall summary.
2324 * This uses prob to reverse the scaling that was done in init_slice_summary().
2325 */
2326void merge_slice_summary(std::vector<double>& dst, const std::vector<double>& src, double prob)
2327{
2328 const unsigned size = src.size();
2329
2330 // Make sure we have enough space.
2331 if(dst.size() < size) {
2332 dst.resize(size, 0.0);
2333 }
2334
2335 // Merge the data.
2336 for(unsigned i = 0; i != size; ++i) {
2337 dst[i] += src[i] * prob;
2338 }
2339}
2340
2341} // end anon namespace
2342
2343// Two man enter. One man leave!
2344// ... Or maybe two. But definitely not three.
2345// Of course, one could be a woman. Or both.
2346// And either could be non-human, too.
2347// Um, ok, it was a stupid thing to say.
2348void combatant::fight(combatant& opponent, bool levelup_considered, bool use_biased_rng)
2349{
2350 // If defender has firststrike and we don't, reverse.
2351 if(opponent.u_.firststrike && !u_.firststrike) {
2352 opponent.fight(*this, levelup_considered, use_biased_rng);
2353 return;
2354 }
2355
2356#ifdef ATTACK_PREDICTION_DEBUG
2357 printf("A:\n");
2358 dump(u_);
2359 printf("B:\n");
2360 dump(opponent.u_);
2361#endif
2362
2363#if 0
2364 std::vector<double> prev = summary[0], opp_prev = opponent.summary[0];
2365 complex_fight(opponent, 1);
2366 std::vector<double> res = summary[0], opp_res = opponent.summary[0];
2367 summary[0] = prev;
2368 opponent.summary[0] = opp_prev;
2369#endif
2370
2371 // The chance so far of not being hit this combat:
2372 double self_not_hit = 1.0;
2373 double opp_not_hit = 1.0;
2374
2375 // The probability of being already dead before the fight begins:
2376 double self_already_dead = hp_dist[0];
2377 double opp_already_dead = opponent.hp_dist[0];
2378
2379 // If incoming slow probabilities are extremely close to 0 or 1, round them to exactly 0 or 1 (bug #3321)
2380 round_prob_if_close_to_sure(slowed);
2381 round_prob_if_close_to_sure(opponent.slowed);
2382
2383 // If we've fought before and we have swarm, we might have to split the
2384 // calculation by number of attacks.
2385 const std::vector<combat_slice> split = split_summary(u_, summary);
2386 const std::vector<combat_slice> opp_split = split_summary(opponent.u_, opponent.summary);
2387
2388 // With the default RNG, optimise by using Monte Carlo when the fight is very complex.
2389 // With the biased RNG, always use Monte Carlo because it's the only implementation.
2390 bool use_monte_carlo_simulation = use_biased_rng
2391 || (fight_complexity(split.size(), opp_split.size(), u_, opponent.u_) > MONTE_CARLO_SIMULATION_THRESHOLD
2392 && prefs::get().damage_prediction_allow_monte_carlo_simulation());
2393
2394 if(use_monte_carlo_simulation) {
2395 complex_fight(use_biased_rng ? attack_prediction_mode::biased_rng_simulation : attack_prediction_mode::monte_carlo_simulation, u_, opponent.u_, u_.num_blows,
2396 opponent.u_.num_blows, summary, opponent.summary, self_not_hit, opp_not_hit, levelup_considered, split,
2397 opp_split, slowed, opponent.slowed);
2398 } else if(split.size() == 1 && opp_split.size() == 1) {
2399 // No special treatment due to swarm is needed. Ignore the split.
2400 do_fight(u_, opponent.u_, u_.num_blows, opponent.u_.num_blows, summary, opponent.summary, self_not_hit,
2401 opp_not_hit, levelup_considered);
2402 } else {
2403 // Storage for the accumulated hit point distributions.
2404 summary_t summary_result, opp_summary_result;
2405 // The chance of not being hit becomes an accumulated chance:
2406 self_not_hit = 0.0;
2407 opp_not_hit = 0.0;
2408
2409 // Loop through all the potential combat situations.
2410 for(unsigned s = 0; s != split.size(); ++s) {
2411 for(unsigned t = 0; t != opp_split.size(); ++t) {
2412 const double sit_prob = split[s].prob * opp_split[t].prob;
2413
2414 // Create summaries for this potential combat situation.
2415 summary_t sit_summary, sit_opp_summary;
2416 init_slice_summary(sit_summary[0], summary[0], split[s].begin_hp, split[s].end_hp, split[s].prob);
2417 init_slice_summary(sit_summary[1], summary[1], split[s].begin_hp, split[s].end_hp, split[s].prob);
2418 init_slice_summary(sit_opp_summary[0], opponent.summary[0], opp_split[t].begin_hp, opp_split[t].end_hp,
2419 opp_split[t].prob);
2420 init_slice_summary(sit_opp_summary[1], opponent.summary[1], opp_split[t].begin_hp, opp_split[t].end_hp,
2421 opp_split[t].prob);
2422
2423 // Scale the "not hit" chance for this situation by the chance that
2424 // this situation applies.
2425 double sit_self_not_hit = sit_prob;
2426 double sit_opp_not_hit = sit_prob;
2427
2428 do_fight(u_, opponent.u_, split[s].strikes, opp_split[t].strikes, sit_summary, sit_opp_summary,
2429 sit_self_not_hit, sit_opp_not_hit, levelup_considered);
2430
2431 // Collect the results.
2432 self_not_hit += sit_self_not_hit;
2433 opp_not_hit += sit_opp_not_hit;
2434 merge_slice_summary(summary_result[0], sit_summary[0], sit_prob);
2435 merge_slice_summary(summary_result[1], sit_summary[1], sit_prob);
2436 merge_slice_summary(opp_summary_result[0], sit_opp_summary[0], sit_prob);
2437 merge_slice_summary(opp_summary_result[1], sit_opp_summary[1], sit_prob);
2438 }
2439 }
2440
2441 // Swap in the results.
2442 summary[0].swap(summary_result[0]);
2443 summary[1].swap(summary_result[1]);
2444 opponent.summary[0].swap(opp_summary_result[0]);
2445 opponent.summary[1].swap(opp_summary_result[1]);
2446 }
2447
2448#if 0
2449 assert(summary[0].size() == res.size());
2450 assert(opponent.summary[0].size() == opp_res.size());
2451 for(unsigned int i = 0; i < summary[0].size(); ++i) {
2452 if(std::fabs(summary[0][i] - res[i]) > 0.000001) {
2453 PLAIN_LOG << "Mismatch for " << i << " hp: " << summary[0][i] << " should have been " << res[i];
2454 assert(false);
2455 }
2456 }
2457 for(unsigned int i = 0; i < opponent.summary[0].size(); ++i) {
2458 if(std::fabs(opponent.summary[0][i] - opp_res[i]) > 0.000001) {
2459 PLAIN_LOG << "Mismatch for " << i << " hp: " << opponent.summary[0][i] << " should have been " << opp_res[i];
2460 assert(false);
2461 }
2462 }
2463#endif
2464
2465 // Combine summary into distribution.
2466 if(summary[1].empty()) {
2467 hp_dist = summary[0];
2468 } else {
2469 const unsigned size = summary[0].size();
2470 hp_dist.resize(size);
2471 for(unsigned int i = 0; i < size; ++i)
2472 hp_dist[i] = summary[0][i] + summary[1][i];
2473 }
2474
2475 if(opponent.summary[1].empty()) {
2476 opponent.hp_dist = opponent.summary[0];
2477 } else {
2478 const unsigned size = opponent.summary[0].size();
2479 opponent.hp_dist.resize(size);
2480 for(unsigned int i = 0; i < size; ++i)
2481 opponent.hp_dist[i] = opponent.summary[0][i] + opponent.summary[1][i];
2482 }
2483
2484 // Chance that we / they were touched this time.
2485 double touched = 1.0 - self_not_hit;
2486 double opp_touched = 1.0 - opp_not_hit;
2487
2488 poisoned = calculate_probability_of_debuff(poisoned, opponent.u_.poisons, touched, 1.0 - hp_dist[0],
2489 u_.experience + game_config::kill_xp(opponent.u_.level) >= u_.max_experience, opponent.hp_dist[0] - opp_already_dead);
2490 opponent.poisoned = calculate_probability_of_debuff(opponent.poisoned, u_.poisons, opp_touched, 1.0 - opponent.hp_dist[0],
2491 opponent.u_.experience + game_config::kill_xp(u_.level) >= opponent.u_.max_experience, hp_dist[0] - self_already_dead);
2492
2493 /* The slowed probability depends on in how many rounds
2494 * the combatant happened to be slowed.
2495 * We need to recalculate it based on the HP distribution.
2496 */
2497 slowed = std::min(std::accumulate(summary[1].begin(), summary[1].end(), 0.0), 1.0);
2498 opponent.slowed = std::min(std::accumulate(opponent.summary[1].begin(), opponent.summary[1].end(), 0.0), 1.0);
2499
2500 // We'll level up after the battle and assume that full-heals -> slow/poison will go away
2502 poisoned = 0.0;
2503 slowed = 0.0;
2504 }
2505 if(opponent.u_.can_advance && opponent.u_.experience + game_config::combat_xp(u_.level) >= opponent.u_.max_experience) {
2506 opponent.poisoned = 0.0;
2507 opponent.slowed = 0.0;
2508 }
2509
2510 untouched *= self_not_hit;
2511 opponent.untouched *= opp_not_hit;
2512}
2513
2514double combatant::average_hp(unsigned int healing) const
2515{
2516 double total = 0;
2517
2518 // Since sum of probabilities is 1.0, we can just tally weights.
2519 for(unsigned int i = 1; i < hp_dist.size(); ++i) {
2520 total += hp_dist[i] * std::min<unsigned int>(i + healing, u_.max_hp);
2521 }
2522
2523 return total;
2524}
2525
2526/* ** The stand-alone program ** */
2527
2528#if defined(BENCHMARK) || defined(CHECK)
2529// We create a significant number of nasty-to-calculate units,
2530// and test each one against the others.
2531static const unsigned int NUM_UNITS = 50;
2532
2533#ifdef ATTACK_PREDICTION_DEBUG
2534void list_combatant(const battle_context_unit_stats& stats, unsigned fighter)
2535{
2536 std::ostringstream ss;
2537
2538 // TODO: swarm_max? not strikes?
2539 ss << "#" << fighter << ": " << stats.swarm_max << "-" << stats.damage << "; "
2540 << stats.chance_to_hit << "% chance to hit; ";
2541
2542 if(stats.drains) {
2543 ss << "drains, ";
2544 }
2545
2546 if(stats.slows) {
2547 ss << "slows, ";
2548 }
2549
2550 if(stats.rounds > 1) {
2551 ss << "berserk, ";
2552 }
2553
2554 if(stats.swarm) {
2555 ss << "swarm(" << stats.num_blows << "), ";
2556 }
2557
2558 if(stats.firststrike) {
2559 ss << "firststrike, ";
2560 }
2561
2562 ss << "max hp = " << stats.max_hp << "\n";
2563
2564 std::cout << ss.rdbuf() << std::endl;
2565}
2566#else
2567void list_combatant(const battle_context_unit_stats&, unsigned)
2568{
2569}
2570#endif
2571
2572#ifdef HUMAN_READABLE
2573void combatant::print(const char label[], unsigned int battle, unsigned int fighter) const
2574{
2575 std::ostringstream ss;
2576
2577 // TODO: add this to the stream... no idea how to convert it properly...
2578 printf("#%06u: (%02u) %s%*c %u-%d; %uhp; %02u%% to hit; %.2f%% unscathed; ", battle, fighter, label,
2579 static_cast<int>(strlen(label)) - 12, ':', u_.swarm_max, u_.damage, u_.hp, u_.chance_to_hit, untouched * 100.0);
2580
2581 if(u_.drains) {
2582 ss << "drains, ";
2583 }
2584
2585 if(u_.slows) {
2586 ss << "slows, ";
2587 }
2588
2589 if(u_.rounds > 1) {
2590 ss << "berserk, ";
2591 }
2592
2593 if(u_.swarm) {
2594 ss << "swarm, ";
2595 }
2596
2597 if(u_.firststrike) {
2598 ss << "firststrike, ";
2599 }
2600
2601 std::cout << ss.rdbuf() << std::endl;
2602
2603 // TODO: add to stream
2604 printf("maxhp=%zu ", hp_dist.size() - 1);
2605
2606 int num_outputs = 0;
2607 for(unsigned int i = 0; i < hp_dist.size(); ++i) {
2608 if(hp_dist[i] != 0.0) {
2609 if(num_outputs++ % 6 == 0) {
2610 printf("\n\t");
2611 } else {
2612 printf(" ");
2613 }
2614
2615 printf("%2u: %5.2f", i, hp_dist[i] * 100);
2616 }
2617 }
2618
2619 printf("\n");
2620}
2621#elif defined(CHECK)
2622void combatant::print(const char label[], unsigned int battle, unsigned int /*fighter*/) const
2623{
2624 std::ostringstream ss;
2625
2626 printf("#%u: %s: %d %u %u %2g%% ", battle, label, u_.damage, u_.swarm_max, u_.hp,
2627 static_cast<float>(u_.chance_to_hit));
2628
2629 if(u_.drains) {
2630 ss << "drains, ";
2631 }
2632
2633 if(u_.slows) {
2634 ss << "slows, ";
2635 }
2636
2637 if(u_.rounds > 1) {
2638 ss << "berserk, ";
2639 }
2640
2641 if(u_.swarm) {
2642 ss << "swarm, ";
2643 }
2644
2645 if(u_.firststrike) {
2646 ss << "firststrike, ";
2647 }
2648
2649 std::cout << ss.rdbuf() << std::endl;
2650
2651 // TODO: add to stream
2652 printf("maxhp=%zu ", hp_dist.size() - 1);
2653 printf(" %.2f", untouched);
2654 for(unsigned int i = 0; i < hp_dist.size(); ++i) {
2655 printf(" %.2f", hp_dist[i] * 100);
2656 }
2657
2658 printf("\n");
2659}
2660#else // ... BENCHMARK
2661void combatant::print(const char /*label*/[], unsigned int /*battle*/, unsigned int /*fighter*/) const
2662{
2663}
2664#endif
2665
2666void combatant::reset()
2667{
2668 for(unsigned int i = 0; i < hp_dist.size(); ++i) {
2669 hp_dist[i] = 0.0;
2670 }
2671
2672 untouched = 1.0;
2673 poisoned = u_.is_poisoned ? 1.0 : 0.0;
2674 slowed = u_.is_slowed ? 1.0 : 0.0;
2675 summary[0] = std::vector<double>();
2676 summary[1] = std::vector<double>();
2677}
2678
2679static void run(unsigned specific_battle)
2680{
2681 using std::chrono::duration_cast;
2682 using std::chrono::microseconds;
2683
2684 // N^2 battles
2685 struct battle_context_unit_stats* stats[NUM_UNITS];
2686 struct combatant* u[NUM_UNITS];
2687 unsigned int i, j, k, battle = 0;
2688 std::chrono::high_resolution_clock::time_point start, end;
2689
2690 for(i = 0; i < NUM_UNITS; ++i) {
2691 unsigned alt = i + 74; // To offset some cycles.
2692 // To get somewhat realistic performance data, try to approximate
2693 // hit point ranges for mainline units (say 25-60 max hitpoints?)
2694 unsigned max_hp = (i * 2) % 23 + (i * 3) % 14 + 25;
2695 unsigned hp = (alt * 5) % max_hp + 1;
2696 stats[i] = new battle_context_unit_stats(alt % 8 + 2, // damage
2697 (alt % 19 + 3) / 4, // number of strikes
2698 hp, max_hp,
2699 (i % 6) * 10 + 30, // hit chance
2700 (i % 13) % 4 == 0, // drains
2701 (i % 11) % 3 == 0, // slows
2702 false, // slowed
2703 i % 7 == 0, // berserk
2704 (i % 17) / 2 == 0, // firststrike
2705 i % 5 == 0); // swarm
2706 u[i] = new combatant(*stats[i]);
2707 list_combatant(*stats[i], i + 1);
2708 }
2709
2710 start = std::chrono::high_resolution_clock::now();
2711 // Go through all fights with two attackers (j and k attacking i).
2712 for(i = 0; i < NUM_UNITS; ++i) {
2713 for(j = 0; j < NUM_UNITS; ++j) {
2714 if(i == j) {
2715 continue;
2716 }
2717
2718 for(k = 0; k < NUM_UNITS; ++k) {
2719 if(i == k || j == k) {
2720 continue;
2721 }
2722
2723 ++battle;
2724 if(specific_battle && battle != specific_battle) {
2725 continue;
2726 }
2727
2728 // Fight!
2729 u[j]->fight(*u[i]);
2730 u[k]->fight(*u[i]);
2731 // Results.
2732 u[i]->print("Defender", battle, i + 1);
2733 u[j]->print("Attacker #1", battle, j + 1);
2734 u[k]->print("Attacker #2", battle, k + 1);
2735 // Start the next fight fresh.
2736 u[i]->reset();
2737 u[j]->reset();
2738 u[k]->reset();
2739 }
2740 }
2741 }
2742
2743 end = std::chrono::high_resolution_clock::now();
2744
2745 auto total = end - start;
2746
2747#ifdef BENCHMARK
2748 printf("Total time for %u combats was %lf\n", NUM_UNITS * (NUM_UNITS - 1) * (NUM_UNITS - 2),
2749 static_cast<double>(duration_cast<microseconds>(total).count()) / 1000000.0);
2750 printf("Time per calc = %li us\n", static_cast<long>(duration_cast<microseconds>(total).count())
2751 / (NUM_UNITS * (NUM_UNITS - 1) * (NUM_UNITS - 2)));
2752#else
2753 printf("Total combats: %u\n", NUM_UNITS * (NUM_UNITS - 1) * (NUM_UNITS - 2));
2754#endif
2755
2756 for(i = 0; i < NUM_UNITS; ++i) {
2757 delete u[i];
2758 delete stats[i];
2759 }
2760
2761 exit(0);
2762}
2763
2764static battle_context_unit_stats* parse_unit(char*** argv)
2765{
2766 // There are four required parameters.
2767 int add_to_argv = 4;
2768 int damage = atoi((*argv)[1]);
2769 int num_attacks = atoi((*argv)[2]);
2770 int hitpoints = atoi((*argv)[3]), max_hp = hitpoints;
2771 int hit_chance = atoi((*argv)[4]);
2772
2773 // Parse the optional (fifth) parameter.
2774 bool drains = false, slows = false, slowed = false, berserk = false, firststrike = false, swarm = false;
2775 if((*argv)[5] && atoi((*argv)[5]) == 0) {
2776 // Optional parameter is present.
2777 ++add_to_argv;
2778
2779 char* max = strstr((*argv)[5], "maxhp=");
2780 if(max) {
2781 max_hp = atoi(max + strlen("maxhp="));
2782 if(max_hp < hitpoints) {
2783 PLAIN_LOG << "maxhp must be at least hitpoints.";
2784 exit(1);
2785 }
2786 }
2787
2788 if(strstr((*argv)[5], "drain")) {
2789 if(!max) {
2790 PLAIN_LOG << "WARNING: drain specified without maxhp; assuming uninjured.";
2791 }
2792
2793 drains = true;
2794 }
2795
2796 if(strstr((*argv)[5], "slows")) {
2797 slows = true;
2798 }
2799
2800 if(strstr((*argv)[5], "slowed")) {
2801 slowed = true;
2802 }
2803
2804 if(strstr((*argv)[5], "berserk")) {
2805 berserk = true;
2806 }
2807
2808 if(strstr((*argv)[5], "firststrike")) {
2809 firststrike = true;
2810 }
2811
2812 if(strstr((*argv)[5], "swarm")) {
2813 if(!max) {
2814 PLAIN_LOG << "WARNING: swarm specified without maxhp; assuming uninjured.";
2815 }
2816
2817 swarm = true;
2818 }
2819 }
2820
2821 // Update argv.
2822 *argv += add_to_argv;
2823
2824 // Construct the stats and return.
2825 return new battle_context_unit_stats(
2826 damage, num_attacks, hitpoints, max_hp, hit_chance, drains, slows, slowed, berserk, firststrike, swarm);
2827}
2828
2829int main(int argc, char* argv[])
2830{
2831 battle_context_unit_stats *def_stats, *att_stats[20];
2832 combatant *def, *att[20];
2833 unsigned int i;
2834
2835 if(argc < 3)
2836 run(argv[1] ? atoi(argv[1]) : 0);
2837
2838 if(argc < 9) {
2839 PLAIN_LOG
2840 << "Usage: " << argv[0] << " [<battle>]\n\t" << argv[0] << " "
2841 << "<damage> <attacks> <hp> <hitprob> [drain,slows,slowed,swarm,firststrike,berserk,maxhp=<num>] "
2842 << "<damage> <attacks> <hp> <hitprob> [drain,slows,slowed,berserk,firststrike,swarm,maxhp=<num>] ...";
2843 exit(1);
2844 }
2845
2846 def_stats = parse_unit(&argv);
2847 def = new combatant(*def_stats);
2848 for(i = 0; argv[1] && i < 19; ++i) {
2849 att_stats[i] = parse_unit(&argv);
2850 att[i] = new combatant(*att_stats[i]);
2851 }
2852
2853 att[i] = nullptr;
2854
2855 for(i = 0; att[i]; ++i) {
2856 debug(("Fighting next attacker\n"));
2857 att[i]->fight(*def);
2858 }
2859
2860 def->print("Defender", 0, 0);
2861 for(i = 0; att[i]; ++i) {
2862 att[i]->print("Attacker", 0, i + 1);
2863 }
2864
2865 for(i = 0; att[i]; ++i) {
2866 delete att[i];
2867 delete att_stats[i];
2868 }
2869
2870 delete def;
2871 delete def_stats;
2872
2873 return 0;
2874}
2875
2876#endif // Standalone program
Various functions that implement attacks and attack calculations.
double t
map_location prev
#define debug(x)
std::vector< std::string > names
static prefs & get()
this class does not give synced random results derived classes might do.
Definition random.hpp:28
static rng & default_instance()
Definition random.cpp:73
T::difference_type get_random_element(T first, T last)
This helper method selects a random element from a container of floating-point numbers.
Definition random.hpp:111
bool get_random_bool(double probability)
This helper method returns true with the probability supplied as a parameter.
Definition random.cpp:136
std::string label
What to show in the filter's drop-down list.
Definition manager.cpp:201
#define PLAIN_LOG
Definition log.hpp:296
int roll_credit(randomness::rng &rng)
Rolls the credit a combatant starts a fight with.
Definition attack.cpp:83
bool roll_hit(randomness::rng &rng, int strikes_left, int chance_to_hit, int &credit)
Rolls one strike.
Definition attack.cpp:88
void clear()
Clear the current render target.
Definition draw.cpp:52
EXIT_STATUS start(bool clear_id, const std::string &filename, bool take_screenshot, const std::string &screenshot_filename)
Main interface for launching the editor from the title screen.
int kill_xp(int level)
int combat_xp(int level)
T end(const std::pair< T, T > &p)
T begin(const std::pair< T, T > &p)
std::size_t size(std::string_view str)
Length in characters of a UTF-8 string.
Definition unicode.cpp:81
int main(int, char **)
Definition sdl3.cpp:25
rect dst
Location on the final composed sheet.
rect src
Non-transparent portion of the surface to compose.
Structure describing the statistics of a unit involved in the battle.
Definition attack.hpp:72
bool slows
Attack slows opponent when it hits.
Definition attack.hpp:78
unsigned int num_blows
Effective number of blows, takes swarm into account.
Definition attack.hpp:99
bool petrifies
Attack petrifies opponent when it hits.
Definition attack.hpp:80
int drain_percent
Percentage of damage recovered as health.
Definition attack.hpp:97
unsigned int hp
Hitpoints of the unit at the beginning of the battle.
Definition attack.hpp:92
int slow_damage
Effective damage if unit becomes slowed (== damage, if already slowed)
Definition attack.hpp:96
unsigned int max_experience
Definition attack.hpp:88
bool drains
Attack drains opponent when it hits.
Definition attack.hpp:79
unsigned int swarm_min
Minimum number of blows with swarm (equal to num_blows if swarm isn't used).
Definition attack.hpp:100
bool swarm
Attack has swarm special.
Definition attack.hpp:83
bool is_attacker
True if the unit is the attacker.
Definition attack.hpp:75
bool is_poisoned
True if the unit is poisoned at the beginning of the battle.
Definition attack.hpp:76
bool is_slowed
True if the unit is slowed at the beginning of the battle.
Definition attack.hpp:77
unsigned int rounds
Berserk special can force us to fight more than one round.
Definition attack.hpp:91
unsigned int swarm_max
Maximum number of blows with swarm (equal to num_blows if swarm isn't used).
Definition attack.hpp:101
unsigned int calc_blows(unsigned new_hp) const
Calculates the number of blows we would have if we had new_hp instead of the recorded hp.
Definition attack.hpp:121
unsigned int max_hp
Maximum hitpoints of the unit.
Definition attack.hpp:93
int damage
Effective damage of the weapon (all factors accounted for).
Definition attack.hpp:95
bool can_advance
The unit can advance.
Definition attack.hpp:86
bool poisons
Attack poisons opponent when it hits.
Definition attack.hpp:82
unsigned int chance_to_hit
Effective chance to hit as a percentage (all factors accounted for).
Definition attack.hpp:94
int drain_constant
Base HP drained regardless of damage dealt.
Definition attack.hpp:98
bool firststrike
Attack has firststrike special.
Definition attack.hpp:84
unsigned int experience
Definition attack.hpp:88
All combat-related info.
double slowed
Resulting chance we are slowed.
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::vector< double > hp_dist
Resulting probability distribution (might be not as large as max_hp)
double poisoned
Resulting chance we are poisoned.
static const unsigned int MONTE_CARLO_SIMULATION_THRESHOLD
const battle_context_unit_stats & u_
std::array< std::vector< double >, 2 > summary
Summary of matrix used to calculate last battle (unslowed & slowed).
double average_hp(unsigned int healing=0) const
What's the average hp (weighted average of hp_dist).
combatant(const battle_context_unit_stats &u, const combatant *prev=nullptr)
Construct a combatant.
double untouched
Resulting chance we were not hit by this opponent (important if it poisons)
bool empty() const
False if both w and h are > 0, true otherwise.
Definition rect.cpp:49
constexpr point size() const
Definition rect.hpp:67
mock_party p
static map_location::direction s
#define i
#define e
#define c