From 1b3eecf8df55e253d1eeb1c274ba5487194fbaa6 Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Fri, 8 Jul 2016 16:58:41 +0300 Subject: [PATCH 01/11] Split the combat_matrix class to two Combat_matrix is now an abstract class, and a new class called probability_combat_matrix got the functionality split away from combat_matrix. I plan to later create a class called monte_carlo_combat_matrix. This way the Monte Carlo combat simulation code can inherit functionality it needs (combat_matrix) but not the functionality it doesn't (probability_combat_matrix). In addition, the calling code can use both classes through the shared interface. --- src/attack_prediction.cpp | 306 ++++++++++++++++++++++---------------- 1 file changed, 178 insertions(+), 128 deletions(-) diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index 755beeef832..3d5274aaf1a 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -752,11 +752,10 @@ void prob_matrix::dump() const /** * A matrix for calculating the outcome of combat. - * This class is concerned with translating things that happen in combat - * to matrix operations that then get passed on to the (private) matrix - * implementation. + * This class specifies the interface and functionality shared between + * probability_combat_matrix and monte_carlo_combat_matrix. */ -class combat_matrix : private prob_matrix +class combat_matrix : protected prob_matrix { public: combat_matrix(unsigned int a_max_hp, unsigned int b_max_hp, @@ -769,12 +768,7 @@ public: int a_drain_percent, int b_drain_percent, int a_drain_constant, int b_drain_constant); - ~combat_matrix() {} - - // A hits B. - void receive_blow_b(double hit_chance); - // B hits A. Why can't they just get along? - void receive_blow_a(double hit_chance); + virtual ~combat_matrix() {} // We lied: actually did less damage, adjust matrix. void remove_petrify_distortion_a(unsigned damage, unsigned slow_damage, unsigned b_hp); @@ -787,19 +781,12 @@ public: void conditional_levelup_b(); // Its over, and here's the bill. - void extract_results(std::vector summary_a[2], - std::vector summary_b[2]); - - /// What is the chance that one of the combatants is dead? - double dead_prob() const { return prob_of_zero(true, true); } - /// What is the chance that combatant 'a' is dead? - double dead_prob_a() const { return prob_of_zero(true, false); } - /// What is the chance that combatant 'b' is dead? - double dead_prob_b() const { return prob_of_zero(false, true); } + virtual void extract_results(std::vector summary_a[2], + std::vector summary_b[2]) = 0; void dump() const { prob_matrix::dump(); } -private: +protected: unsigned a_max_hp_; bool a_slows_; unsigned a_damage_; @@ -847,9 +834,139 @@ combat_matrix::combat_matrix(unsigned int a_max_hp, unsigned int b_max_hp, { } +// We lied: actually did less damage, adjust matrix. +void combat_matrix::remove_petrify_distortion_a(unsigned damage, unsigned slow_damage, + unsigned b_hp) +{ + for (int p = 0; p < NUM_PLANES; ++p) { + if (!plane_used(p)) + continue; + + // A is slow in planes 1 and 3. + unsigned actual_damage = (p & 1) ? slow_damage : damage; + if (b_hp > actual_damage) + // B was actually petrified, not killed. + move_column(p, p, b_hp - actual_damage, 0); + } +} + +void combat_matrix::remove_petrify_distortion_b(unsigned damage, unsigned slow_damage, + unsigned a_hp) +{ + for (int p = 0; p < NUM_PLANES; ++p) { + if (!plane_used(p)) + continue; + + // B is slow in planes 2 and 3. + unsigned actual_damage = (p & 2) ? slow_damage : damage; + if (a_hp > actual_damage) + // A was actually petrified, not killed. + move_row(p, p, a_hp - actual_damage, 0); + } +} + +void combat_matrix::forced_levelup_a() +{ + /* Move all the values (except 0hp) of all the planes to the "fully healed" + row of the planes unslowed for A. */ + for (int p = 0; p < NUM_PLANES; ++p) { + if (plane_used(p)) + merge_cols(p & -2, p, a_max_hp_); + } +} + +void combat_matrix::forced_levelup_b() +{ + /* Move all the values (except 0hp) of all the planes to the "fully healed" + column of planes unslowed for B. */ + for (int p = 0; p < NUM_PLANES; ++p) { + if (plane_used(p)) + merge_rows(p & -3, p, b_max_hp_); + } +} + +void combat_matrix::conditional_levelup_a() +{ + /* Move the values of the first column (except 0hp) of all the + planes to the "fully healed" row of the planes unslowed for A. */ + for (int p = 0; p < NUM_PLANES; ++p) { + if (plane_used(p)) + merge_col(p & -2, p, 0, a_max_hp_); + } +} + +void combat_matrix::conditional_levelup_b() +{ + /* Move the values of the first row (except 0hp) of all the + planes to the last column of the planes unslowed for B. */ + for (int p = 0; p < NUM_PLANES; ++p) { + if (plane_used(p)) + merge_row(p & -3, p, 0, b_max_hp_); + } +} + +/** + * Implementation of combat_matrix that calculates exact probabilities of events. + * Fast in "simple" fights (low number of strikes, low HP, and preferably no slow + * or snare effect), but can be unusably expensive in extremely complex situations. + */ +class probability_combat_matrix : public combat_matrix +{ +public: + probability_combat_matrix(unsigned int a_max_hp, unsigned int b_max_hp, + unsigned int a_hp, unsigned int b_hp, + const std::vector a_summary[2], + const std::vector b_summary[2], + bool a_slows, bool b_slows, + unsigned int a_damage, unsigned int b_damage, + unsigned int a_slow_damage, unsigned int b_slow_damage, + int a_drain_percent, int b_drain_percent, + int a_drain_constant, int b_drain_constant); + + // A hits B. + void receive_blow_b(double hit_chance); + // B hits A. Why can't they just get along? + void receive_blow_a(double hit_chance); + + /// What is the chance that one of the combatants is dead? + double dead_prob() const { return prob_of_zero(true, true); } + /// What is the chance that combatant 'a' is dead? + double dead_prob_a() const { return prob_of_zero(true, false); } + /// What is the chance that combatant 'b' is dead? + double dead_prob_b() const { return prob_of_zero(false, true); } + + void extract_results(std::vector summary_a[2], + std::vector summary_b[2]) override; +}; + +/** + * Constructor. + * @param a_max_hp The maximum hit points for A. + * @param b_max_hp The maximum hit points for B. + * @param a_slows Set to true if A slows B when A hits B. + * @param b_slows Set to true if B slows A when B hits A. + * @param a_hp The current hit points for A. (Ignored if a_summary[0] is not empty.) + * @param b_hp The current hit points for B. (Ignored if b_summary[0] is not empty.) + * @param a_summary The hit point distribution for A (from previous combats). Element [0] is for normal A. while [1] is for slowed A. + * @param b_summary The hit point distribution for B (from previous combats). Element [0] is for normal B. while [1] is for slowed B. + */ +probability_combat_matrix::probability_combat_matrix(unsigned int a_max_hp, unsigned int b_max_hp, + unsigned int a_hp, unsigned int b_hp, + const std::vector a_summary[2], + const std::vector b_summary[2], + bool a_slows, bool b_slows, + unsigned int a_damage, unsigned int b_damage, + unsigned int a_slow_damage, unsigned int b_slow_damage, + int a_drain_percent, int b_drain_percent, + int a_drain_constant, int b_drain_constant) + : combat_matrix(a_max_hp, b_max_hp, a_hp, b_hp, a_summary, b_summary, a_slows, b_slows, + a_damage, b_damage, a_slow_damage, b_slow_damage, a_drain_percent, b_drain_percent, a_drain_constant, b_drain_constant) +{ +} + // Shift combat_matrix to reflect the probability 'hit_chance' that damage // is done to 'b'. -void combat_matrix::receive_blow_b(double hit_chance) +void probability_combat_matrix::receive_blow_b(double hit_chance) { // Walk backwards so we don't copy already-copied matrix planes. unsigned src = NUM_PLANES; @@ -869,7 +986,7 @@ void combat_matrix::receive_blow_b(double hit_chance) // Shift matrix to reflect probability 'hit_chance' // that damage (up to) 'damage' is done to 'a'. -void combat_matrix::receive_blow_a(double hit_chance) +void probability_combat_matrix::receive_blow_a(double hit_chance) { // Walk backwards so we don't copy already-copied matrix planes. unsigned src = NUM_PLANES; @@ -887,79 +1004,8 @@ void combat_matrix::receive_blow_a(double hit_chance) } } -// We lied: actually did less damage, adjust matrix. -void combat_matrix::remove_petrify_distortion_a(unsigned damage, unsigned slow_damage, - unsigned b_hp) -{ - for (int p = 0; p < NUM_PLANES; ++p) { - if ( !plane_used(p) ) - continue; - - // A is slow in planes 1 and 3. - unsigned actual_damage = (p & 1) ? slow_damage : damage; - if ( b_hp > actual_damage ) - // B was actually petrified, not killed. - move_column(p, p, b_hp - actual_damage, 0); - } -} - -void combat_matrix::remove_petrify_distortion_b(unsigned damage, unsigned slow_damage, - unsigned a_hp) -{ - for (int p = 0; p < NUM_PLANES; ++p) { - if ( !plane_used(p) ) - continue; - - // B is slow in planes 2 and 3. - unsigned actual_damage = (p & 2) ? slow_damage : damage; - if ( a_hp > actual_damage ) - // A was actually petrified, not killed. - move_row(p, p, a_hp - actual_damage, 0); - } -} - -void combat_matrix::forced_levelup_a() -{ - /* Move all the values (except 0hp) of all the planes to the "fully healed" - row of the planes unslowed for A. */ - for (int p = 0; p < NUM_PLANES; ++p) { - if ( plane_used(p) ) - merge_cols(p & -2, p, a_max_hp_); - } -} - -void combat_matrix::forced_levelup_b() -{ - /* Move all the values (except 0hp) of all the planes to the "fully healed" - column of planes unslowed for B. */ - for (int p = 0; p < NUM_PLANES; ++p) { - if ( plane_used(p) ) - merge_rows(p & -3, p, b_max_hp_); - } -} - -void combat_matrix::conditional_levelup_a() -{ - /* Move the values of the first column (except 0hp) of all the - planes to the "fully healed" row of the planes unslowed for A. */ - for (int p = 0; p < NUM_PLANES; ++p) { - if ( plane_used(p) ) - merge_col(p & -2, p, 0, a_max_hp_); - } -} - -void combat_matrix::conditional_levelup_b() -{ - /* Move the values of the first row (except 0hp) of all the - planes to the last column of the planes unslowed for B. */ - for (int p = 0; p < NUM_PLANES; ++p) { - if ( plane_used(p) ) - merge_row(p & -3, p, 0, b_max_hp_); - } -} - -void combat_matrix::extract_results(std::vector summary_a[2], - std::vector summary_b[2]) +void probability_combat_matrix::extract_results(std::vector summary_a[2], + std::vector summary_b[2]) { // Reset the summaries. summary_a[0] = std::vector(num_rows()); @@ -1367,57 +1413,61 @@ void complex_fight(const battle_context_unit_stats &stats, b_damage = b_slow_damage = stats.max_hp; // Prepare the matrix that will do our calculations. - combat_matrix m(stats.max_hp, opp_stats.max_hp, - stats.hp, opp_stats.hp, summary, opp_summary, - stats.slows && !opp_stats.is_slowed, - opp_stats.slows && !stats.is_slowed, - a_damage, b_damage, a_slow_damage, b_slow_damage, - stats.drain_percent, opp_stats.drain_percent, - stats.drain_constant, opp_stats.drain_constant); - const double hit_chance = stats.chance_to_hit / 100.0; - const double opp_hit_chance = opp_stats.chance_to_hit / 100.0; + std::unique_ptr m; + { + probability_combat_matrix* pm = new probability_combat_matrix(stats.max_hp, opp_stats.max_hp, + stats.hp, opp_stats.hp, summary, opp_summary, + stats.slows && !opp_stats.is_slowed, + opp_stats.slows && !stats.is_slowed, + a_damage, b_damage, a_slow_damage, b_slow_damage, + stats.drain_percent, opp_stats.drain_percent, + stats.drain_constant, opp_stats.drain_constant); + m.reset(pm); + const double hit_chance = stats.chance_to_hit / 100.0; + const double opp_hit_chance = opp_stats.chance_to_hit / 100.0; - do { - for (unsigned int i = 0; i < max_attacks; ++i) { - if ( i < strikes ) { - debug(("A strikes\n")); - opp_not_hit *= 1.0 - hit_chance*(1.0-m.dead_prob_a()); - m.receive_blow_b(hit_chance); - m.dump(); + do { + for (unsigned int i = 0; i < max_attacks; ++i) { + if (i < strikes) { + debug(("A strikes\n")); + opp_not_hit *= 1.0 - hit_chance*(1.0 - pm->dead_prob_a()); + pm->receive_blow_b(hit_chance); + pm->dump(); + } + if (i < opp_strikes) { + debug(("B strikes\n")); + self_not_hit *= 1.0 - opp_hit_chance*(1.0 - pm->dead_prob_b()); + pm->receive_blow_a(opp_hit_chance); + pm->dump(); + } } - if ( i < opp_strikes ) { - debug(("B strikes\n")); - self_not_hit *= 1.0 - opp_hit_chance*(1.0-m.dead_prob_b()); - m.receive_blow_a(opp_hit_chance); - m.dump(); - } - } - debug(("Combat ends:\n")); - m.dump(); - } while (--rounds && m.dead_prob() < 0.99); + debug(("Combat ends:\n")); + pm->dump(); + } while (--rounds && pm->dead_prob() < 0.99); + } if (stats.petrifies) - m.remove_petrify_distortion_a(stats.damage, stats.slow_damage, opp_stats.hp); + m->remove_petrify_distortion_a(stats.damage, stats.slow_damage, opp_stats.hp); if (opp_stats.petrifies) - m.remove_petrify_distortion_b(opp_stats.damage, opp_stats.slow_damage, stats.hp); + m->remove_petrify_distortion_b(opp_stats.damage, opp_stats.slow_damage, stats.hp); if (levelup_considered) { if ( stats.experience + opp_stats.level >= stats.max_experience ) { - m.forced_levelup_a(); + m->forced_levelup_a(); } else if ( stats.experience + game_config::kill_xp(opp_stats.level) >= stats.max_experience ) { - m.conditional_levelup_a(); + m->conditional_levelup_a(); } if ( opp_stats.experience + stats.level >= opp_stats.max_experience ) { - m.forced_levelup_b(); + m->forced_levelup_b(); } else if ( opp_stats.experience + game_config::kill_xp(stats.level) >= opp_stats.max_experience ) { - m.conditional_levelup_b(); + m->conditional_levelup_b(); } } // We extract results separately, then combine. - m.extract_results(summary, opp_summary); + m->extract_results(summary, opp_summary); } From 93761bcd1e67f13b4de09779338cb1c807e914f9 Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Sat, 9 Jul 2016 12:37:55 +0300 Subject: [PATCH 02/11] Initial implementation of the Monte Carlo damage prediction mode The implementation isn't yet wired to the high-level damage preview calculation code, or even tested for that matter. I also extended the random number generator by adding functions get_random_bool(), get_random_float() and get_random_element(). --- src/attack_prediction.cpp | 506 +++++++++++++++++++++++++++++--------- src/attack_prediction.hpp | 10 +- src/random_new.cpp | 25 ++ src/random_new.hpp | 50 +++- 4 files changed, 470 insertions(+), 121 deletions(-) diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index 3d5274aaf1a..869546a6a47 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -38,7 +38,9 @@ #include "actions/attack.hpp" #include "game_config.hpp" +#include "random_new.hpp" #include +#include #if defined(BENCHMARK) || defined(CHECK) #include @@ -109,6 +111,127 @@ const T& limit(const T& val, const T& min, const T& max) return val; } +/** +* A struct to describe one possible combat scenario. +* (Needed when the number of attacks can vary due to swarm.) +*/ +struct combat_slice +{ + // The hit point range this slice covers. + unsigned begin_hp; // included in the range. + unsigned end_hp; // excluded from the range. + + // The probability of this slice. + double prob; + + // The number of strikes applicable with this slice. + unsigned strikes; + + + combat_slice(const std::vector src_summary[2], + unsigned begin, unsigned end, unsigned num_strikes); + combat_slice(const std::vector src_summary[2], unsigned num_strikes); +}; + + +/** +* Creates a slice from a summary, and associates a number of strikes. +*/ +combat_slice::combat_slice(const std::vector src_summary[2], + unsigned begin, unsigned end, + unsigned num_strikes) : + begin_hp(begin), + end_hp(end), + prob(0.0), + strikes(num_strikes) +{ + if (src_summary[0].empty()) { + // No summary; this should be the only slice. + prob = 1.0; + return; + } + + // Avoid accessing beyond the end of the vectors. + if (end > src_summary[0].size()) + end = src_summary[0].size(); + + // Sum the probabilities in the slice. + for (unsigned i = begin; i < end; ++i) + prob += src_summary[0][i]; + if (!src_summary[1].empty()) + for (unsigned i = begin; i < end; ++i) + prob += src_summary[1][i]; +} + + +/** +* Creates a slice from the summaries, and associates a number of strikes. +* This version of the constructor creates a slice consisting of everything. +*/ +combat_slice::combat_slice(const std::vector src_summary[2], + unsigned num_strikes) : + begin_hp(0), + end_hp(src_summary[0].size()), + prob(1.0), + strikes(num_strikes) +{ +} + +/** +* Returns the number of hit points greater than cur_hp, and at most +* stats.max_hp+1, at which the unit would get another attack because +* of swarm. +* Helper function for split_summary(). +*/ +unsigned hp_for_next_attack(unsigned cur_hp, + const battle_context_unit_stats & stats) +{ + unsigned old_strikes = stats.calc_blows(cur_hp); + + // A formula would have to deal with rounding issues; instead + // loop until we find more strikes. + while (++cur_hp <= stats.max_hp) + if (stats.calc_blows(cur_hp) != old_strikes) + break; + + return cur_hp; +} + +/** +* Split the combat by number of attacks per combatant (for swarm). +* This also clears the current summaries. +*/ +std::vector split_summary(const battle_context_unit_stats& unit_stats, std::vector summary[2]) +{ + std::vector result; + + if (unit_stats.swarm_min == unit_stats.swarm_max || summary[0].empty()) + { + // We use the same number of blows for all possibilities. + result.push_back(combat_slice(summary, unit_stats.num_blows)); + return result; + } + + debug(("Slicing:\n")); + // Loop through our slices. + unsigned cur_end = 0; + do { + // Advance to the next slice. + const unsigned cur_begin = cur_end; + cur_end = hp_for_next_attack(cur_begin, unit_stats); + + // Add this slice. + combat_slice slice(summary, cur_begin, cur_end, unit_stats.calc_blows(cur_begin)); + if (slice.prob != 0.0) { + result.push_back(slice); + debug(("\t%2u-%2u hp; strikes: %u; probability: %6.2f\n", + cur_begin, cur_end, slice.strikes, slice.prob*100.0)); + } + } while (cur_end <= unit_stats.max_hp); + + return result; +} + /** * A matrix of A's hitpoints vs B's hitpoints vs. their slowed states. * This class is concerned only with the matrix implementation and @@ -148,6 +271,15 @@ public: void merge_row(unsigned d_plane, unsigned s_plane, unsigned row, unsigned d_col); void merge_rows(unsigned d_plane, unsigned s_plane, unsigned d_col); + // Set all values to zero and clear the lists of used columns/rows. + void clear(); + + // Record the result of a single Monte Carlo simulation iteration. + void record_monte_carlo_result(unsigned int a_hp, unsigned int b_hp, bool a_slowed, bool b_slowed); + + // Returns the index of the plane with the given slow statuses. + static unsigned int plane_index(bool a_slowed, bool b_slowed) { return a_slowed * 1u + b_slowed * 2u; } + /// What is the chance that an indicated combatant (one of them) is at zero? double prob_of_zero(bool check_a, bool check_b) const; /// Sums the values in the specified plane. @@ -671,6 +803,49 @@ void prob_matrix::merge_rows(unsigned d_plane, unsigned s_plane, unsigned d_col) xfer(d_plane, s_plane, *row_it, d_col, *row_it, *col_it); } +/** + * Set all values to zero and clear the lists of used columns/rows. + */ +void prob_matrix::clear() +{ + if (used_rows_.empty()) + { + // Nothing to do + return; + } + + for (unsigned int p = 0u; p < NUM_PLANES; ++p) + { + if (!plane_used(p)) + { + continue; + } + + decltype(used_rows_[p].begin()) first_row, last_row; + std::tie(first_row, last_row) = std::minmax_element(used_rows_[p].begin(), used_rows_[p].end()); + for (unsigned int r = *first_row; r < *last_row; ++r) + { + for (unsigned int c = 0u; c < cols_; ++c) + { + plane_[p][r * cols_ + c] = 0.0; + } + } + + used_rows_[p].clear(); + used_cols_[p].clear(); + } +} + +/** + * Record the result of a single Monte Carlo simulation iteration. + */ +void prob_matrix::record_monte_carlo_result(unsigned int a_hp, unsigned int b_hp, bool a_slowed, bool b_slowed) +{ + assert(a_hp <= rows_); + assert(b_hp <= cols_); + ++val(plane_index(a_slowed, b_slowed), a_hp, b_hp); +} + /** * What is the chance that an indicated combatant (one of them) is at zero? */ @@ -908,7 +1083,7 @@ void combat_matrix::conditional_levelup_b() /** * Implementation of combat_matrix that calculates exact probabilities of events. * Fast in "simple" fights (low number of strikes, low HP, and preferably no slow - * or snare effect), but can be unusably expensive in extremely complex situations. + * or swarm effect), but can be unusably expensive in extremely complex situations. */ class probability_combat_matrix : public combat_matrix { @@ -1028,74 +1203,241 @@ void probability_combat_matrix::extract_results(std::vector summary_a[2] } } -} // end anon namespace - - /** - * A struct to describe one possible combat scenario. - * (Needed when the number of attacks can vary due to swarm.) + * Implementation of combat_matrix based on Monte Carlo simulation. + * This does not give exact results, but the error should be small + * thanks to the law of large numbers. Probably more important is that + * the simulation time doesn't depend on anything other than the number + * of strikes, which makes this method much faster if the combatants + * have a lot of HP. */ -struct combatant::combat_slice +class monte_carlo_combat_matrix : public combat_matrix { - // The hit point range this slice covers. - unsigned begin_hp; // included in the range. - unsigned end_hp; // excluded from the range. +public: + monte_carlo_combat_matrix(unsigned int a_max_hp, unsigned int b_max_hp, + unsigned int a_hp, unsigned int b_hp, + const std::vector a_summary[2], + const std::vector b_summary[2], + bool a_slows, bool b_slows, + unsigned int a_damage, unsigned int b_damage, + unsigned int a_slow_damage, unsigned int b_slow_damage, + int a_drain_percent, int b_drain_percent, + int a_drain_constant, int b_drain_constant, + unsigned int rounds, + double a_hit_chance, double b_hit_chance, + std::vector a_split, std::vector b_split, + double a_initially_slowed_chance, double b_initially_slowed_chance); - // The probability of this slice. - double prob; + void simulate(); - // The number of strikes applicable with this slice. - unsigned strikes; + void extract_results(std::vector summary_a[2], + std::vector summary_b[2]) override; +private: + static const unsigned int NUM_ITERATIONS = 5000u; - combat_slice(const std::vector src_summary[2], - unsigned begin, unsigned end, unsigned num_strikes); - combat_slice(const std::vector src_summary[2], unsigned num_strikes); + std::vector a_initial_; + std::vector b_initial_; + std::vector a_initial_slowed_; + std::vector b_initial_slowed_; + std::vector a_split_; + std::vector b_split_; + unsigned int rounds_; + double a_hit_chance_; + double b_hit_chance_; + double a_initially_slowed_chance_; + double b_initially_slowed_chance_; + + unsigned int calc_blows_a(unsigned int a_hp) const; + unsigned int calc_blows_b(unsigned int b_hp) const; + static void divide_all_elements(std::vector& vec, double divisor); + static void scale_probabilities(const std::vector& source, std::vector& target, double multiplier, unsigned int singular_hp); }; -/** - * Creates a slice from a summary, and associates a number of strikes. - */ -combatant::combat_slice::combat_slice(const std::vector src_summary[2], - unsigned begin, unsigned end, - unsigned num_strikes) : - begin_hp(begin), - end_hp(end), - prob(0.0), - strikes(num_strikes) +monte_carlo_combat_matrix::monte_carlo_combat_matrix(unsigned int a_max_hp, unsigned int b_max_hp, + unsigned int a_hp, unsigned int b_hp, + const std::vector a_summary[2], + const std::vector b_summary[2], + bool a_slows, bool b_slows, + unsigned int a_damage, unsigned int b_damage, + unsigned int a_slow_damage, unsigned int b_slow_damage, + int a_drain_percent, int b_drain_percent, + int a_drain_constant, int b_drain_constant, + unsigned int rounds, + double a_hit_chance, double b_hit_chance, + std::vector a_split, std::vector b_split, + double a_initially_slowed_chance, double b_initially_slowed_chance) + : combat_matrix(a_max_hp, b_max_hp, a_hp, b_hp, a_summary, b_summary, a_slows, b_slows, + a_damage, b_damage, a_slow_damage, b_slow_damage, a_drain_percent, b_drain_percent, a_drain_constant, b_drain_constant), + + rounds_(rounds), a_hit_chance_(a_hit_chance), b_hit_chance_(b_hit_chance), a_split_(a_split), b_split_(b_split), + a_initially_slowed_chance_(a_initially_slowed_chance), b_initially_slowed_chance_(b_initially_slowed_chance) { - if ( src_summary[0].empty() ) { - // No summary; this should be the only slice. - prob = 1.0; - return; + scale_probabilities(a_summary[0], a_initial_, 1.0 / (1.0 - a_initially_slowed_chance), a_hp); + scale_probabilities(a_summary[1], a_initial_slowed_, 1.0 / a_initially_slowed_chance, a_hp); + scale_probabilities(b_summary[0], b_initial_, 1.0 / (1.0 - b_initially_slowed_chance), b_hp); + scale_probabilities(b_summary[1], b_initial_slowed_, 1.0 / b_initially_slowed_chance, b_hp); + + clear(); +} + +void monte_carlo_combat_matrix::simulate() +{ + for (unsigned int i = 0u; i < NUM_ITERATIONS; ++i) + { + bool a_slowed = random_new::generator->get_random_bool(a_initially_slowed_chance_); + bool b_slowed = random_new::generator->get_random_bool(b_initially_slowed_chance_); + const std::vector& a_initial = a_slowed ? a_initial_slowed_ : a_initial_; + const std::vector& b_initial = b_slowed ? b_initial_slowed_ : b_initial_; + unsigned int a_hp = random_new::generator->get_random_element(a_initial.begin(), a_initial.end()); + unsigned int b_hp = random_new::generator->get_random_element(b_initial.begin(), b_initial.end()); + unsigned int a_strikes = calc_blows_a(a_hp); + unsigned int b_strikes = calc_blows_b(b_hp); + + for (unsigned int j = 0u; j < rounds_ && a_hp > 0u && b_hp > 0u; ++j) + { + for (unsigned int k = 0u; k < std::max(a_strikes, b_strikes); ++k) + { + if (k < a_strikes) + { + if (random_new::generator->get_random_bool(a_hit_chance_)) + { + // A hits B + unsigned int damage = a_slowed ? a_slow_damage_ : a_damage_; + damage = std::min(damage, b_hp); + b_slowed |= a_slows_; + + int drain_amount = (a_drain_percent_ * static_cast(damage) / 100 + a_drain_constant_); + a_hp = limit(a_hp + drain_amount, 1u, a_max_hp_); + + b_hp -= damage; + + if (b_hp == 0u) + { + // A killed B + break; + } + } + } + + if (k < b_strikes) + { + if (random_new::generator->get_random_bool(b_hit_chance_)) + { + // B hits A + unsigned int damage = b_slowed ? b_slow_damage_ : b_damage_; + damage = std::min(damage, a_hp); + a_slowed |= b_slows_; + + int drain_amount = (b_drain_percent_ * static_cast(damage) / 100 + b_drain_constant_); + b_hp = limit(b_hp + drain_amount, 1u, b_max_hp_); + + a_hp -= damage; + + if (a_hp == 0u) + { + // B killed A + break; + } + } + } + } + } + + record_monte_carlo_result(a_hp, b_hp, a_slowed, b_slowed); + } +} + +/** + * Otherwise the same as in probability_combat_matrix, but this needs to divide the values + * by the number of iterations. + */ +void monte_carlo_combat_matrix::extract_results(std::vector summary_a[2], + std::vector summary_b[2]) +{ + // Reset the summaries. + summary_a[0] = std::vector(num_rows()); + summary_b[0] = std::vector(num_cols()); + + if (plane_used(A_SLOWED)) + summary_a[1] = std::vector(num_rows()); + if (plane_used(B_SLOWED)) + summary_b[1] = std::vector(num_cols()); + + for (unsigned p = 0; p < NUM_PLANES; ++p) { + if (!plane_used(p)) + continue; + + // A is slow in planes 1 and 3. + unsigned dst_a = (p & 1) ? 1u : 0u; + // B is slow in planes 2 and 3. + unsigned dst_b = (p & 2) ? 1u : 0u; + sum(p, summary_a[dst_a], summary_b[dst_b]); } - // Avoid accessing beyond the end of the vectors. - if ( end > src_summary[0].size() ) - end = src_summary[0].size(); + divide_all_elements(summary_a[0], static_cast(NUM_ITERATIONS)); + divide_all_elements(summary_b[0], static_cast(NUM_ITERATIONS)); - // Sum the probabilities in the slice. - for ( unsigned i = begin; i < end; ++i ) - prob += src_summary[0][i]; - if ( !src_summary[1].empty() ) - for ( unsigned i = begin; i < end; ++i ) - prob += src_summary[1][i]; + if (plane_used(A_SLOWED)) + divide_all_elements(summary_a[1], static_cast(NUM_ITERATIONS)); + + if (plane_used(B_SLOWED)) + divide_all_elements(summary_b[1], static_cast(NUM_ITERATIONS)); } - -/** - * Creates a slice from the summaries, and associates a number of strikes. - * This version of the constructor creates a slice consisting of everything. - */ -combatant::combat_slice::combat_slice(const std::vector src_summary[2], - unsigned num_strikes) : - begin_hp(0), - end_hp(src_summary[0].size()), - prob(1.0), - strikes(num_strikes) +unsigned int monte_carlo_combat_matrix::calc_blows_a(unsigned int a_hp) const { + auto it = a_split_.begin(); + while (it != a_split_.end() && it->end_hp <= a_hp) + { + ++it; + } + if (it == a_split_.end()) + { + --it; + } + return it->strikes; } +unsigned int monte_carlo_combat_matrix::calc_blows_b(unsigned int b_hp) const +{ + auto it = b_split_.begin(); + while (it != b_split_.end() && it->end_hp <= b_hp) + { + ++it; + } + if (it == b_split_.end()) + { + --it; + } + return it->strikes; +} + +void monte_carlo_combat_matrix::scale_probabilities(const std::vector& source, std::vector& target, double multiplier, unsigned int singular_hp) +{ + if (source.empty()) + { + target.resize(singular_hp + 1u, 0.0); + target[singular_hp] = 1.0; + } + else + { + std::transform(source.begin(), source.end(), std::back_inserter(target), [=](double prob){ return multiplier * prob; }); + } + + assert(std::abs(std::accumulate(target.begin(), target.end(), 0.0) - 1.0) < 0.001); +} + +void monte_carlo_combat_matrix::divide_all_elements(std::vector& vec, double divisor) +{ + for (double& e : vec) + { + e /= divisor; + } +} + +} // end anon namespace + combatant::combatant(const battle_context_unit_stats &u, const combatant *prev) : hp_dist(u.max_hp + 1, 0.0), @@ -1129,64 +1471,6 @@ combatant::combatant(const combatant &that, const battle_context_unit_stats &u) } -namespace { - /** - * Returns the number of hit points greater than cur_hp, and at most - * stats.max_hp+1, at which the unit would get another attack because - * of swarm. - * Helper function for split_summary(). - */ - unsigned hp_for_next_attack(unsigned cur_hp, - const battle_context_unit_stats & stats) - { - unsigned old_strikes = stats.calc_blows(cur_hp); - - // A formula would have to deal with rounding issues; instead - // loop until we find more strikes. - while ( ++cur_hp <= stats.max_hp ) - if ( stats.calc_blows(cur_hp) != old_strikes ) - break; - - return cur_hp; - } -} // end anon namespace - -/** - * Split the combat by number of attacks per combatant (for swarm). - * This also clears the current summaries. - */ -std::vector combatant::split_summary() const -{ - std::vector result; - - if ( u_.swarm_min == u_.swarm_max || summary[0].empty() ) - { - // We use the same number of blows for all possibilities. - result.push_back(combat_slice(summary, u_.num_blows)); - return result; - } - - debug(("Slicing:\n")); - // Loop through our slices. - unsigned cur_end = 0; - do { - // Advance to the next slice. - const unsigned cur_begin = cur_end; - cur_end = hp_for_next_attack(cur_begin, u_); - - // Add this slice. - combat_slice slice(summary, cur_begin, cur_end, u_.calc_blows(cur_begin)); - if ( slice.prob != 0.0 ) { - result.push_back(slice); - debug(("\t%2u-%2u hp; strikes: %u; probability: %6.2f\n", - cur_begin, cur_end, slice.strikes, slice.prob*100.0)); - } - } while ( cur_end <= u_.max_hp ); - - return result; -} - - namespace { void forced_levelup(std::vector &hp_dist) @@ -1587,8 +1871,8 @@ void combatant::fight(combatant &opp, bool levelup_considered) // If we've fought before and we have swarm, we might have to split the // calculation by number of attacks. - const std::vector split = split_summary(); - const std::vector opp_split = opp.split_summary(); + const std::vector split = split_summary(u_, summary); + const std::vector opp_split = split_summary(opp.u_, opp.summary); if ( split.size() == 1 && opp_split.size() == 1 ) // No special treatment due to swarm is needed. Ignore the split. diff --git a/src/attack_prediction.hpp b/src/attack_prediction.hpp index 6561103ab99..f11518985e5 100644 --- a/src/attack_prediction.hpp +++ b/src/attack_prediction.hpp @@ -35,6 +35,9 @@ struct combatant /** Copy constructor */ combatant(const combatant &that, const battle_context_unit_stats &u); + combatant(const combatant &that) = delete; + combatant& operator=(const combatant &) = delete; + /** Simulate a fight! Can be called multiple times for cumulative calculations. */ void fight(combatant &opponent, bool levelup_considered=true); @@ -60,13 +63,6 @@ struct combatant #endif private: - combatant(const combatant &that); - combatant& operator=(const combatant &); - - struct combat_slice; - /** Split the combat by number of attacks per combatant (for swarm). */ - std::vector split_summary() const; - const battle_context_unit_stats &u_; /** Summary of matrix used to calculate last battle (unslowed & slowed). diff --git a/src/random_new.cpp b/src/random_new.cpp index 15770e97634..96e4e676daf 100644 --- a/src/random_new.cpp +++ b/src/random_new.cpp @@ -99,4 +99,29 @@ namespace random_new assert(max >= 0); return static_cast (next_random() % (static_cast(max)+1)); } + + double rng::get_random_double() + { + uint64_t double_as_int = 0u; + /* Exponent. It's set to zero. + Exponent bias is 1023 in double precision, and therefore the value 1023 + needs to be encoded. */ + double_as_int |= static_cast(1023) << 52; + /* Significand. A double-precision floating point number stores 52 significand bits. + The underlying RNG only gives us 32 bits, so we need to shift the bits 20 positions + to the left. The last 20 significand bits we can leave at zero, we don't need + the full 52 bits of randomness allowed by the double-precision format. */ + double_as_int |= static_cast(next_random()) << (52 - 32); + /* At this point, the exponent is zero. The significand, taking into account the + implicit leading one bit, is at least exactly one and at most almost two. + In other words, a reinterpret_cast gives us a number in the range [1, 2[. + Simply subtract one from that value and return it. */ + return *reinterpret_cast(&double_as_int) - 1.0; + } + + bool rng::get_random_bool(double probability) + { + assert(probability >= 0.0 && probability <= 1.0); + return get_random_double() < probability; + } } diff --git a/src/random_new.hpp b/src/random_new.hpp index 75d962c1a67..16a9e9a645c 100644 --- a/src/random_new.hpp +++ b/src/random_new.hpp @@ -15,9 +15,7 @@ #define RANDOM_NEW_H_INCLUDED #include //needed for RAND_MAX -#include - -using boost::uint32_t; +#include namespace random_new { @@ -49,6 +47,30 @@ namespace random_new */ int get_random_int(int min, int max) { return min + get_random_int_in_range_zero_to(max - min); } + + /** + * This helper method returns true with the probability supplied as a parameter. + * @param probability The probability of returning true, from 0 to 1. + */ + bool get_random_bool(double probability); + + /** + * This helper method returns a floating-point number in the range [0,1[. + */ + double get_random_double(); + + /** + * This helper method selects a random element from a container of floating-point numbers. + * Every number has a probability to be selected equal to the number itself + * (e.g. a number of 0.1 is selected with a probability of 0.1). The sum of numbers + * should be one. + * @param first Iterator to the beginning of the container + * @param last Iterator to the end of the container + * @ret The index of the selected number + */ + template + unsigned int get_random_element(T first, T last); + static rng& default_instance(); protected: virtual uint32_t next_random_impl() = 0; @@ -68,5 +90,27 @@ namespace random_new Outside a synced context this has the same effect as rand() */ extern rng* generator; + + template + unsigned int rng::get_random_element(T first, T last) + { + double target = get_random_double(); + double sum = 0.0; + T it = first; + sum += *it; + while (sum <= target) + { + ++it; + if (it != last) + { + sum += *it; + } + else + { + break; + } + } + return std::distance(first, it); + } } #endif From cbbc665c45261fcc19d8026a9ec3835f26ffe489 Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Wed, 13 Jul 2016 08:29:56 +0300 Subject: [PATCH 03/11] Minor bug fixes and code quality improvements --- src/attack_prediction.cpp | 42 +++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index 869546a6a47..7469bf22433 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -13,6 +13,8 @@ Full algorithm by Yogin. Original typing and optimization by Rusty. + Monte Carlo simulation mode implemented by Jyrki Vesterinen. + This code has lots of debugging. It is there for a reason: this code is kinda tricky. Do not remove it. */ @@ -40,6 +42,7 @@ #include "game_config.hpp" #include "random_new.hpp" #include +#include #include #if defined(BENCHMARK) || defined(CHECK) @@ -808,12 +811,6 @@ void prob_matrix::merge_rows(unsigned d_plane, unsigned s_plane, unsigned d_col) */ void prob_matrix::clear() { - if (used_rows_.empty()) - { - // Nothing to do - return; - } - for (unsigned int p = 0u; p < NUM_PLANES; ++p) { if (!plane_used(p)) @@ -821,6 +818,12 @@ void prob_matrix::clear() continue; } + if (used_rows_[p].empty()) + { + // Nothing to do + continue; + } + decltype(used_rows_[p].begin()) first_row, last_row; std::tie(first_row, last_row) = std::minmax_element(used_rows_[p].begin(), used_rows_[p].end()); for (unsigned int r = *first_row; r < *last_row; ++r) @@ -1215,18 +1218,18 @@ class monte_carlo_combat_matrix : public combat_matrix { public: monte_carlo_combat_matrix(unsigned int a_max_hp, unsigned int b_max_hp, - unsigned int a_hp, unsigned int b_hp, - const std::vector a_summary[2], - const std::vector b_summary[2], + unsigned int a_hp, unsigned int b_hp, + const std::vector a_summary[2], + const std::vector b_summary[2], bool a_slows, bool b_slows, unsigned int a_damage, unsigned int b_damage, unsigned int a_slow_damage, unsigned int b_slow_damage, int a_drain_percent, int b_drain_percent, int a_drain_constant, int b_drain_constant, - unsigned int rounds, - double a_hit_chance, double b_hit_chance, - std::vector a_split, std::vector b_split, - double a_initially_slowed_chance, double b_initially_slowed_chance); + unsigned int rounds, + double a_hit_chance, double b_hit_chance, + std::vector a_split, std::vector b_split, + double a_initially_slowed_chance, double b_initially_slowed_chance); void simulate(); @@ -1415,6 +1418,13 @@ unsigned int monte_carlo_combat_matrix::calc_blows_b(unsigned int b_hp) const void monte_carlo_combat_matrix::scale_probabilities(const std::vector& source, std::vector& target, double multiplier, unsigned int singular_hp) { + if (std::isinf(multiplier)) + { + // Happens if the "target" HP distribution vector isn't used, + // in which case it's not necessary to scale the probabilities. + return; + } + if (source.empty()) { target.resize(singular_hp + 1u, 0.0); @@ -1701,6 +1711,12 @@ void complex_fight(const battle_context_unit_stats &stats, { probability_combat_matrix* pm = new probability_combat_matrix(stats.max_hp, opp_stats.max_hp, stats.hp, opp_stats.hp, summary, opp_summary, + /* FIXME: due to this stupid optimization (not creating the slowed planes if the + combatant is already slowed), the combatant summary will have a different meaning than + usual if the combatant was already slowed (summary[0] will mean the slowed health + distribution, and summary[1] will be empty). That, in turn, will break the damage + calculation if another battle is later predicted for the same unit using Monte Carlo + simulation, since the MC simulation mode requires that summary has its usual meaning. */ stats.slows && !opp_stats.is_slowed, opp_stats.slows && !stats.is_slowed, a_damage, b_damage, a_slow_damage, b_slow_damage, From 2c04802a7daecec106bd4807aa336e4023ca69ac Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Wed, 13 Jul 2016 20:54:13 +0300 Subject: [PATCH 04/11] Use Monte Carlo damage calculation in very complex fights --- src/attack_prediction.cpp | 109 +++++++++++++++++++++++++++++++++----- src/attack_prediction.hpp | 2 + 2 files changed, 99 insertions(+), 12 deletions(-) diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index 7469bf22433..9e90c688c4b 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -846,7 +846,10 @@ void prob_matrix::record_monte_carlo_result(unsigned int a_hp, unsigned int b_hp { assert(a_hp <= rows_); assert(b_hp <= cols_); - ++val(plane_index(a_slowed, b_slowed), a_hp, b_hp); + unsigned int plane = plane_index(a_slowed, b_slowed); + ++val(plane, a_hp, b_hp); + used_rows_[plane].insert(a_hp); + used_cols_[plane].insert(b_hp); } /** @@ -1483,6 +1486,12 @@ combatant::combatant(const combatant &that, const battle_context_unit_stats &u) namespace { +enum class attack_prediction_mode +{ + probability_calculation, + monte_carlo_simulation +}; + void forced_levelup(std::vector &hp_dist) { /* If we survive the combat, we will level up. So the probability @@ -1528,6 +1537,26 @@ unsigned min_hp(const std::vector & hp_dist, unsigned def) return def; } +/** + * Returns a number that approximates the complexity of the fight, + * for the purpose of determining if it's faster to calculate exact + * probabilities or to run a Monte Carlo simulation. + * Ignores the numbers of rounds and strikes because these slow down + * both calculation modes. + */ +unsigned int fight_complexity(unsigned int num_slices, + unsigned int opp_num_slices, + const battle_context_unit_stats& stats, + const battle_context_unit_stats& opp_stats) +{ + return num_slices * + opp_num_slices * + (stats.slows || opp_stats.is_slowed) ? 2 : 1 * + (opp_stats.slows || stats.is_slowed) ? 2 : 1 * + stats.max_hp * + opp_stats.max_hp; +} + // Combat without chance of death, berserk, slow or drain is simple. void no_death_fight(const battle_context_unit_stats &stats, const battle_context_unit_stats &opp_stats, @@ -1682,13 +1711,20 @@ void one_strike_fight(const battle_context_unit_stats &stats, } } -void complex_fight(const battle_context_unit_stats &stats, +/* The parameters "split", "opp_split", "initially_slowed_chance" and +"opp_initially_slowed_chance" are ignored in the probability calculation mode. */ +void complex_fight(attack_prediction_mode mode, + const battle_context_unit_stats &stats, const battle_context_unit_stats &opp_stats, unsigned strikes, unsigned opp_strikes, std::vector summary[2], std::vector opp_summary[2], double & self_not_hit, double & opp_not_hit, - bool levelup_considered) + bool levelup_considered, + std::vector split, + std::vector opp_split, + double initially_slowed_chance, + double opp_initially_slowed_chance) { unsigned int rounds = std::max(stats.rounds, opp_stats.rounds); unsigned max_attacks = std::max(strikes, opp_strikes); @@ -1706,9 +1742,15 @@ void complex_fight(const battle_context_unit_stats &stats, if (opp_stats.petrifies) b_damage = b_slow_damage = stats.max_hp; + const double hit_chance = stats.chance_to_hit / 100.0; + const double opp_hit_chance = opp_stats.chance_to_hit / 100.0; + // Prepare the matrix that will do our calculations. std::unique_ptr m; + if (mode == attack_prediction_mode::probability_calculation) { + debug(("Using exact probability calculations.\n")); + probability_combat_matrix* pm = new probability_combat_matrix(stats.max_hp, opp_stats.max_hp, stats.hp, opp_stats.hp, summary, opp_summary, /* FIXME: due to this stupid optimization (not creating the slowed planes if the @@ -1723,8 +1765,6 @@ void complex_fight(const battle_context_unit_stats &stats, stats.drain_percent, opp_stats.drain_percent, stats.drain_constant, opp_stats.drain_constant); m.reset(pm); - const double hit_chance = stats.chance_to_hit / 100.0; - const double opp_hit_chance = opp_stats.chance_to_hit / 100.0; do { for (unsigned int i = 0; i < max_attacks; ++i) { @@ -1746,6 +1786,30 @@ void complex_fight(const battle_context_unit_stats &stats, pm->dump(); } while (--rounds && pm->dead_prob() < 0.99); } + else + { + debug(("Using Monte Carlo simulation.\n")); + + monte_carlo_combat_matrix* mcm = new monte_carlo_combat_matrix(stats.max_hp, opp_stats.max_hp, + stats.hp, opp_stats.hp, summary, opp_summary, + stats.slows || opp_stats.is_slowed, + opp_stats.slows || stats.is_slowed, + a_damage, b_damage, a_slow_damage, b_slow_damage, + stats.drain_percent, opp_stats.drain_percent, + stats.drain_constant, opp_stats.drain_constant, + rounds, + hit_chance, opp_hit_chance, + split, opp_split, + initially_slowed_chance, + opp_initially_slowed_chance); + m.reset(mcm); + + mcm->simulate(); + debug(("Combat ends:\n")); + mcm->dump(); + + // TODO: update hit probabilities + } if (stats.petrifies) m->remove_petrify_distortion_a(stats.damage, stats.slow_damage, opp_stats.hp); @@ -1800,14 +1864,22 @@ void do_fight(const battle_context_unit_stats &stats, summary[0], opp_summary[0], self_not_hit, opp_not_hit, levelup_considered); else - complex_fight(stats, opp_stats, strikes, opp_strikes, + complex_fight(attack_prediction_mode::probability_calculation, + stats, opp_stats, strikes, opp_strikes, summary, opp_summary, self_not_hit, opp_not_hit, - levelup_considered); + levelup_considered, + std::vector(), + std::vector(), + 0.0, 0.0); } else - complex_fight(stats, opp_stats, strikes, opp_strikes, + complex_fight(attack_prediction_mode::probability_calculation, + stats, opp_stats, strikes, opp_strikes, summary, opp_summary, self_not_hit, opp_not_hit, - levelup_considered); + levelup_considered, + std::vector(), + std::vector(), + 0.0, 0.0); } /** @@ -1890,11 +1962,24 @@ void combatant::fight(combatant &opp, bool levelup_considered) const std::vector split = split_summary(u_, summary); const std::vector opp_split = split_summary(opp.u_, opp.summary); - if ( split.size() == 1 && opp_split.size() == 1 ) + if (fight_complexity(split.size(), opp_split.size(), u_, opp.u_) > + MONTE_CARLO_SIMULATION_THRESHOLD) + { + // A very complex fight. Use Monte Carlo simulation instead of exact + // probability calculations. + complex_fight(attack_prediction_mode::monte_carlo_simulation, + u_, opp.u_, u_.num_blows, opp.u_.num_blows, + summary, opp.summary, self_not_hit, opp_not_hit, + levelup_considered, + split, opp_split, slowed, opp.slowed); + } + else if (split.size() == 1 && opp_split.size() == 1) + { // No special treatment due to swarm is needed. Ignore the split. do_fight(u_, opp.u_, u_.num_blows, opp.u_.num_blows, - summary, opp.summary, self_not_hit, opp_not_hit, - levelup_considered); + summary, opp.summary, self_not_hit, opp_not_hit, + levelup_considered); + } else { // Storage for the accumulated hit point distributions. diff --git a/src/attack_prediction.hpp b/src/attack_prediction.hpp index f11518985e5..264661da63c 100644 --- a/src/attack_prediction.hpp +++ b/src/attack_prediction.hpp @@ -63,6 +63,8 @@ struct combatant #endif private: + static const unsigned int MONTE_CARLO_SIMULATION_THRESHOLD = 5000u; + const battle_context_unit_stats &u_; /** Summary of matrix used to calculate last battle (unslowed & slowed). From ddf842194446917847203c8e3afa0d04f24b50a2 Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Thu, 14 Jul 2016 23:31:03 +0300 Subject: [PATCH 05/11] Various improvements * Use the C++11 std::chrono functionality for damage prediction code benchmarking. This way the stand-alone testing/benchmarking code can be compiled on any platform. * Implement calculating the probability to be hit. * Fix: prob_matrix::clear() didn't fully clear the matrix. --- src/attack_prediction.cpp | 59 ++++++++++++++++++++++++--------------- 1 file changed, 37 insertions(+), 22 deletions(-) diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index 9e90c688c4b..058937d787f 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -46,8 +46,7 @@ #include #if defined(BENCHMARK) || defined(CHECK) -#include -#include +#include #include #include @@ -826,7 +825,7 @@ void prob_matrix::clear() decltype(used_rows_[p].begin()) first_row, last_row; std::tie(first_row, last_row) = std::minmax_element(used_rows_[p].begin(), used_rows_[p].end()); - for (unsigned int r = *first_row; r < *last_row; ++r) + for (unsigned int r = *first_row; r <= *last_row; ++r) { for (unsigned int c = 0u; c < cols_; ++c) { @@ -1239,6 +1238,9 @@ public: void extract_results(std::vector summary_a[2], std::vector summary_b[2]) override; + double get_a_hit_probability() const; + double get_b_hit_probability() const; + private: static const unsigned int NUM_ITERATIONS = 5000u; @@ -1253,6 +1255,8 @@ private: double b_hit_chance_; double a_initially_slowed_chance_; double b_initially_slowed_chance_; + unsigned int iterations_a_hit_ = 0u; + unsigned int iterations_b_hit_ = 0u; unsigned int calc_blows_a(unsigned int a_hp) const; unsigned int calc_blows_b(unsigned int b_hp) const; @@ -1291,6 +1295,8 @@ void monte_carlo_combat_matrix::simulate() { for (unsigned int i = 0u; i < NUM_ITERATIONS; ++i) { + bool a_hit = false; + bool b_hit = false; bool a_slowed = random_new::generator->get_random_bool(a_initially_slowed_chance_); bool b_slowed = random_new::generator->get_random_bool(b_initially_slowed_chance_); const std::vector& a_initial = a_slowed ? a_initial_slowed_ : a_initial_; @@ -1311,6 +1317,7 @@ void monte_carlo_combat_matrix::simulate() // A hits B unsigned int damage = a_slowed ? a_slow_damage_ : a_damage_; damage = std::min(damage, b_hp); + b_hit = true; b_slowed |= a_slows_; int drain_amount = (a_drain_percent_ * static_cast(damage) / 100 + a_drain_constant_); @@ -1333,6 +1340,7 @@ void monte_carlo_combat_matrix::simulate() // B hits A unsigned int damage = b_slowed ? b_slow_damage_ : b_damage_; damage = std::min(damage, a_hp); + a_hit = true; a_slowed |= b_slows_; int drain_amount = (b_drain_percent_ * static_cast(damage) / 100 + b_drain_constant_); @@ -1350,6 +1358,9 @@ void monte_carlo_combat_matrix::simulate() } } + iterations_a_hit_ += a_hit ? 1 : 0; + iterations_b_hit_ += b_hit ? 1 : 0; + record_monte_carlo_result(a_hp, b_hp, a_slowed, b_slowed); } } @@ -1391,6 +1402,16 @@ void monte_carlo_combat_matrix::extract_results(std::vector summary_a[2] divide_all_elements(summary_b[1], static_cast(NUM_ITERATIONS)); } +double monte_carlo_combat_matrix::get_a_hit_probability() const +{ + return static_cast(iterations_a_hit_) / static_cast(NUM_ITERATIONS); +} + +double monte_carlo_combat_matrix::get_b_hit_probability() const +{ + return static_cast(iterations_b_hit_) / static_cast(NUM_ITERATIONS); +} + unsigned int monte_carlo_combat_matrix::calc_blows_a(unsigned int a_hp) const { auto it = a_split_.begin(); @@ -1808,7 +1829,8 @@ void complex_fight(attack_prediction_mode mode, debug(("Combat ends:\n")); mcm->dump(); - // TODO: update hit probabilities + self_not_hit = 1.0 - mcm->get_a_hit_probability(); + opp_not_hit = 1.0 - mcm->get_b_hit_probability(); } if (stats.petrifies) @@ -2101,17 +2123,6 @@ double combatant::average_hp(unsigned int healing) const // and test each one against the others. #define NUM_UNITS 50 -// Stolen from glibc headers sys/time.h -#define timer_sub(a, b, result) \ - do { \ - (result)->tv_sec = (a)->tv_sec - (b)->tv_sec; \ - (result)->tv_usec = (a)->tv_usec - (b)->tv_usec; \ - if ((result)->tv_usec < 0) { \ - --(result)->tv_sec; \ - (result)->tv_usec += 1000000; \ - } \ - } while (0) - #ifdef ATTACK_PREDICTION_DEBUG void list_combatant(const battle_context_unit_stats & stats, unsigned fighter) @@ -2208,11 +2219,14 @@ void combatant::reset() static void run(unsigned specific_battle) { + using std::chrono::duration_cast; + using std::chrono::microseconds; + // N^2 battles struct battle_context_unit_stats *stats[NUM_UNITS]; struct combatant *u[NUM_UNITS]; unsigned int i, j, k, battle = 0; - struct timeval start, end, total; + std::chrono::high_resolution_clock::time_point start, end; for (i = 0; i < NUM_UNITS; ++i) { unsigned alt = i + 74; // To offset some cycles. @@ -2234,7 +2248,7 @@ static void run(unsigned specific_battle) list_combatant(*stats[i], i+1); } - gettimeofday(&start, nullptr); + start = std::chrono::high_resolution_clock::now(); // Go through all fights with two attackers (j and k attacking i). for (i = 0; i < NUM_UNITS; ++i) { for (j = 0; j < NUM_UNITS; ++j) { @@ -2260,15 +2274,16 @@ static void run(unsigned specific_battle) } } } - gettimeofday(&end, nullptr); + end = std::chrono::high_resolution_clock::now(); - timer_sub(&end, &start, &total); + auto total = end - start; #ifdef BENCHMARK - printf("Total time for %i combats was %lu.%06lu\n", - NUM_UNITS*(NUM_UNITS-1)*(NUM_UNITS-2), total.tv_sec, total.tv_usec); + printf("Total time for %i combats was %lf\n", + NUM_UNITS*(NUM_UNITS-1)*(NUM_UNITS-2), + static_cast(duration_cast(total).count()) / 1000000.0); printf("Time per calc = %li us\n", - ((end.tv_sec-start.tv_sec)*1000000 + (end.tv_usec-start.tv_usec)) + static_cast(duration_cast(total).count()) / (NUM_UNITS*(NUM_UNITS-1)*(NUM_UNITS-2))); #else printf("Total combats: %i\n", NUM_UNITS*(NUM_UNITS-1)*(NUM_UNITS-2)); From c43f70418fe442b6a9eb5790d9eca011aea7aa78 Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Fri, 15 Jul 2016 16:02:12 +0300 Subject: [PATCH 06/11] Fix operator precedence issue The fight_complexity() function was returning way too low values. --- src/attack_prediction.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index 058937d787f..0aceb4c5de4 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -1572,8 +1572,8 @@ unsigned int fight_complexity(unsigned int num_slices, { return num_slices * opp_num_slices * - (stats.slows || opp_stats.is_slowed) ? 2 : 1 * - (opp_stats.slows || stats.is_slowed) ? 2 : 1 * + ((stats.slows || opp_stats.is_slowed) ? 2 : 1) * + ((opp_stats.slows || stats.is_slowed) ? 2 : 1) * stats.max_hp * opp_stats.max_hp; } From d83e0176899cd935befae000cfae9f7deb23b55f Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Sat, 16 Jul 2016 13:13:48 +0300 Subject: [PATCH 07/11] Remove a performance optimization The attack prediction code used to skip creating the "A slowed" or "B slowed" plane if A or B was already slowed. As a result, after the battle the "summary" array of the combatant had a different meaning from usual: normally summary[0] means the HP distribution when the combatant is not slowed, and summary[1] means the HP distribution when the combatant is slowed. However, if the combatant was already slowed before the battle, summary[0] was the HP distribution when the combatant is slowed and summary[1] was empty. This optimization didn't break anything, thanks to itself: because the "A slowed" or "B slowed" plane wasn't used, the code fetched the HP distribution from summary[0] instead of summary[1] and therefore obtained the correct distribution. However, my Monte Carlo simulation code requires that summary[0] and summary[1] have their usual meanings. I decided that the optimization is too hard to support, and removed it. --- src/attack_prediction.cpp | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index 0aceb4c5de4..60e5a08524d 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -398,16 +398,15 @@ prob_matrix::prob_matrix(unsigned int a_max, unsigned int b_max, if ( !b_initial[1].empty() ) initialize_plane(B_SLOWED, a_cur, b_cur, a_initial[0], b_initial[1]); if ( !a_initial[1].empty() && !b_initial[1].empty() ) - // Currently will not happen, but to be complete... initialize_plane(BOTH_SLOWED, a_cur, b_cur, a_initial[1], b_initial[1]); // Some debugging messages. if ( !a_initial[0].empty() ) { - debug(("A has fought before.\n")); + debug(("A has fought before (or is slowed).\n")); dump(); } - else if ( !b_initial[0].empty() ) { - debug(("B has fought before.\n")); + if ( !b_initial[0].empty() ) { + debug(("B has fought before (or is slowed).\n")); dump(); } } @@ -1493,6 +1492,14 @@ combatant::combatant(const battle_context_unit_stats &u, const combatant *prev) untouched = 1.0; poisoned = u.is_poisoned ? 1.0 : 0.0; slowed = u.is_slowed ? 1.0 : 0.0; + + // If we're already slowed, create summary[1] so that probability calculation code + // knows that we're slowed. + if (u.is_slowed) + { + summary[0].resize(u.max_hp + 1, 0.0); + summary[1] = hp_dist; + } } } @@ -1774,14 +1781,8 @@ void complex_fight(attack_prediction_mode mode, probability_combat_matrix* pm = new probability_combat_matrix(stats.max_hp, opp_stats.max_hp, stats.hp, opp_stats.hp, summary, opp_summary, - /* FIXME: due to this stupid optimization (not creating the slowed planes if the - combatant is already slowed), the combatant summary will have a different meaning than - usual if the combatant was already slowed (summary[0] will mean the slowed health - distribution, and summary[1] will be empty). That, in turn, will break the damage - calculation if another battle is later predicted for the same unit using Monte Carlo - simulation, since the MC simulation mode requires that summary has its usual meaning. */ - stats.slows && !opp_stats.is_slowed, - opp_stats.slows && !stats.is_slowed, + stats.slows || opp_stats.is_slowed, + opp_stats.slows || stats.is_slowed, a_damage, b_damage, a_slow_damage, b_slow_damage, stats.drain_percent, opp_stats.drain_percent, stats.drain_constant, opp_stats.drain_constant); From 6766e05747947157f583584112c2fff733671180 Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Sun, 17 Jul 2016 15:27:31 +0300 Subject: [PATCH 08/11] Put the Monte Carlo mode behind an advanced preference and update changelog --- changelog | 9 +++++++++ data/advanced_preferences.cfg | 8 ++++++++ players_changelog | 10 ++++++++++ src/attack_prediction.cpp | 4 +++- src/preferences.cpp | 10 ++++++++++ src/preferences.hpp | 3 +++ 6 files changed, 43 insertions(+), 1 deletion(-) diff --git a/changelog b/changelog index f6d6625ba36..0aa90964d33 100644 --- a/changelog +++ b/changelog @@ -49,6 +49,15 @@ Version 1.13.4+dev: loggers activated in the gui print just like loggers activated in the command line (i.e. messages appear in the console) * Fix bug #24762: Editor actions are out of sync after resizing. + * Performance: + * Added an advanced preference (Preferences -> Advanced -> + Allow damage calculation with Monte Carlo simulation) that, when enabled, + allows the damage calculation to operate by simulating a few thousand fights + instead of calculating exact probabilities of each outcome (when a heuristic + determines that it's probably faster). This method is inexact, but in very + complex battles (extremely high HP, drain, slow, berserk, etc.) it's + significantly faster than the default damage calculation method, and may be + necessary for acceptable performance. * WML engine: * Add color= attribute to [message]. * Add [else], search_recall_list=, auto_recall= to [role] diff --git a/data/advanced_preferences.cfg b/data/advanced_preferences.cfg index 75e1ae2b307..3cdd0b0af15 100644 --- a/data/advanced_preferences.cfg +++ b/data/advanced_preferences.cfg @@ -188,6 +188,14 @@ type=custom [/advanced_preference] +[advanced_preference] + field=damage_prediction_allow_monte_carlo_simulation + name= _ "Allow damage calculation with Monte Carlo simulation" + description= _ "Allow the damage calculation window to simulate fights instead of using exact probability calculations" + type=boolean + default=no +[/advanced_preference] + #ifdef __UNUSED__ [advanced_preference] field=joystick_support_enabled diff --git a/players_changelog b/players_changelog index 0f657cfac5c..ed28e9e2004 100644 --- a/players_changelog +++ b/players_changelog @@ -32,6 +32,16 @@ Version 1.13.4+dev: * Added "Registered users only" checkbox to multiplayer configuration dialog which when checked, only allows registered users to join the game + * Performance: + * Added an advanced preference (Preferences -> Advanced -> + Allow damage calculation with Monte Carlo simulation) that, when enabled, + allows the damage calculation to operate by simulating a few thousand fights + instead of calculating exact probabilities of each outcome (when a heuristic + determines that it's probably faster). This method is inexact, but in very + complex battles (extremely high HP, drain, slow, berserk, etc.) it's + significantly faster than the default damage calculation method, and may be + necessary for acceptable performance. + Version 1.13.4: * Language and i18n: * Updated translations: British English, Russian. diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index 60e5a08524d..9bd6fae1815 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -40,6 +40,7 @@ #include "actions/attack.hpp" #include "game_config.hpp" +#include "preferences.hpp" #include "random_new.hpp" #include #include @@ -1986,7 +1987,8 @@ void combatant::fight(combatant &opp, bool levelup_considered) const std::vector opp_split = split_summary(opp.u_, opp.summary); if (fight_complexity(split.size(), opp_split.size(), u_, opp.u_) > - MONTE_CARLO_SIMULATION_THRESHOLD) + MONTE_CARLO_SIMULATION_THRESHOLD && + preferences::damage_prediction_allow_monte_carlo_simulation()) { // A very complex fight. Use Monte Carlo simulation instead of exact // probability calculations. diff --git a/src/preferences.cpp b/src/preferences.cpp index 2f21da5374a..d5d9f9753a6 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -1041,5 +1041,15 @@ void set_disable_loadingscreen_animation(bool value) set("disable_loadingscreen_animation", value); } +bool damage_prediction_allow_monte_carlo_simulation() +{ + return get("damage_prediction_allow_monte_carlo_simulation", false); +} + +void set_damage_prediction_allow_monte_carlo_simulation(bool value) +{ + set("damage_prediction_allow_monte_carlo_simulation", value); +} + } // end namespace preferences diff --git a/src/preferences.hpp b/src/preferences.hpp index 60b74d342d1..699d883247e 100644 --- a/src/preferences.hpp +++ b/src/preferences.hpp @@ -263,6 +263,9 @@ namespace preferences { bool disable_loadingscreen_animation(); void set_disable_loadingscreen_animation(bool value); + bool damage_prediction_allow_monte_carlo_simulation(); + void set_damage_prediction_allow_monte_carlo_simulation(bool value); + } // end namespace preferences #endif From 9e10291df5b34eb858cd07fdaf22886aa4a4a70f Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Sun, 17 Jul 2016 16:55:44 +0300 Subject: [PATCH 09/11] Attempted fix for build failure with -Werror=reorder --- src/attack_prediction.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index 9bd6fae1815..52c0d356ed1 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -1280,7 +1280,7 @@ monte_carlo_combat_matrix::monte_carlo_combat_matrix(unsigned int a_max_hp, unsi : combat_matrix(a_max_hp, b_max_hp, a_hp, b_hp, a_summary, b_summary, a_slows, b_slows, a_damage, b_damage, a_slow_damage, b_slow_damage, a_drain_percent, b_drain_percent, a_drain_constant, b_drain_constant), - rounds_(rounds), a_hit_chance_(a_hit_chance), b_hit_chance_(b_hit_chance), a_split_(a_split), b_split_(b_split), + a_split_(a_split), b_split_(b_split), rounds_(rounds), a_hit_chance_(a_hit_chance), b_hit_chance_(b_hit_chance), a_initially_slowed_chance_(a_initially_slowed_chance), b_initially_slowed_chance_(b_initially_slowed_chance) { scale_probabilities(a_summary[0], a_initial_, 1.0 / (1.0 - a_initially_slowed_chance), a_hp); From 1bf58b83f425a44123d2bf9b925c1f42063952a7 Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Sun, 17 Jul 2016 17:31:22 +0300 Subject: [PATCH 10/11] Fixed build with GCC and Clang --- src/random_new.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/random_new.hpp b/src/random_new.hpp index 16a9e9a645c..88756a9a43a 100644 --- a/src/random_new.hpp +++ b/src/random_new.hpp @@ -16,6 +16,7 @@ #include //needed for RAND_MAX #include +#include //needed for std::distance namespace random_new { From 0e7305017e3649f460febe867e9610930721d900 Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Thu, 21 Jul 2016 20:41:04 +0300 Subject: [PATCH 11/11] Enabled Monte Carlo simulation mode by default --- changelog | 13 +++++-------- data/advanced_preferences.cfg | 2 +- players_changelog | 13 +++++-------- src/preferences.cpp | 2 +- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/changelog b/changelog index 0aa90964d33..e3615b18778 100644 --- a/changelog +++ b/changelog @@ -50,14 +50,11 @@ Version 1.13.4+dev: command line (i.e. messages appear in the console) * Fix bug #24762: Editor actions are out of sync after resizing. * Performance: - * Added an advanced preference (Preferences -> Advanced -> - Allow damage calculation with Monte Carlo simulation) that, when enabled, - allows the damage calculation to operate by simulating a few thousand fights - instead of calculating exact probabilities of each outcome (when a heuristic - determines that it's probably faster). This method is inexact, but in very - complex battles (extremely high HP, drain, slow, berserk, etc.) it's - significantly faster than the default damage calculation method, and may be - necessary for acceptable performance. + * When a heuristic determines that it's probably faster, the game predicts battle + outcome by simulating a few thousand fights instead of calculating exact + probabilities. This method is inexact, but in very complex battles (extremely + high HP, drain, slow, berserk, etc.) it's significantly faster than the default + damage calculation method. * WML engine: * Add color= attribute to [message]. * Add [else], search_recall_list=, auto_recall= to [role] diff --git a/data/advanced_preferences.cfg b/data/advanced_preferences.cfg index 3cdd0b0af15..30b4ea60e7d 100644 --- a/data/advanced_preferences.cfg +++ b/data/advanced_preferences.cfg @@ -193,7 +193,7 @@ name= _ "Allow damage calculation with Monte Carlo simulation" description= _ "Allow the damage calculation window to simulate fights instead of using exact probability calculations" type=boolean - default=no + default=yes [/advanced_preference] #ifdef __UNUSED__ diff --git a/players_changelog b/players_changelog index ed28e9e2004..075ab381324 100644 --- a/players_changelog +++ b/players_changelog @@ -33,14 +33,11 @@ Version 1.13.4+dev: when checked, only allows registered users to join the game * Performance: - * Added an advanced preference (Preferences -> Advanced -> - Allow damage calculation with Monte Carlo simulation) that, when enabled, - allows the damage calculation to operate by simulating a few thousand fights - instead of calculating exact probabilities of each outcome (when a heuristic - determines that it's probably faster). This method is inexact, but in very - complex battles (extremely high HP, drain, slow, berserk, etc.) it's - significantly faster than the default damage calculation method, and may be - necessary for acceptable performance. + * When a heuristic determines that it's probably faster, the game predicts battle + outcome by simulating a few thousand fights instead of calculating exact + probabilities. This method is inexact, but in very complex battles (extremely + high HP, drain, slow, berserk, etc.) it's significantly faster than the default + damage calculation method. Version 1.13.4: * Language and i18n: diff --git a/src/preferences.cpp b/src/preferences.cpp index d5d9f9753a6..31b2a7bdb9f 100644 --- a/src/preferences.cpp +++ b/src/preferences.cpp @@ -1043,7 +1043,7 @@ void set_disable_loadingscreen_animation(bool value) bool damage_prediction_allow_monte_carlo_simulation() { - return get("damage_prediction_allow_monte_carlo_simulation", false); + return get("damage_prediction_allow_monte_carlo_simulation", true); } void set_damage_prediction_allow_monte_carlo_simulation(bool value)