From 472b0cbbfbcdde4e32f26a1cb5afaa5ffd6c7a88 Mon Sep 17 00:00:00 2001 From: Jyrki Vesterinen Date: Mon, 28 May 2018 22:05:08 +0300 Subject: [PATCH] Fix #3042: attack prediction gives wrong results for HP <= 0 units One_strike_fight() assumed that if HP distribution hadn't been calculated, the unit is alive. It would normally be a valid assumption, but the Wesnoth engine allows units with negative HP (although things aren't guaranteed to work correctly in the presence of such units). The assumption, together with a completely wrong calculation for the probability that the opponent will counterattack, resulted in badly incorrect results. That, in turn, caused the calculated probability that the opponent to kill us to become negative (I observed -75 % when debugging), making the calculated probability to be poisoned/slowed to exceed 100 %, and that finally caused an assert if the AI simulated another fight for the same unit. I have now fixed those issues. I also noticed that rounding error allowed the probability to be killed to still become slightly negative, and thus changed std::min() to utils::clamp() to limit the value to the allowed range. --- src/attack_prediction.cpp | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/attack_prediction.cpp b/src/attack_prediction.cpp index c58731b582ff..99650dc3903f 100644 --- a/src/attack_prediction.cpp +++ b/src/attack_prediction.cpp @@ -1769,8 +1769,9 @@ double calculate_probability_of_debuff(double initial_prob, bool enemy_gives, do // Prob_stay_alive can get slightly negative because of a rounding error, so ensure that it gets non-negative. prob_stay_alive = std::max(prob_stay_alive, 0.0); // 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. + // Likewise, it can get slightly negative if the unit already has negative HP. // Simply limit it to suitable range. - prob_kill = std::min(prob_kill, 1.0); + prob_kill = utils::clamp(prob_kill, 0.0, 1.0); // Probability we are already debuffed and the enemy doesn't hit us. const double prob_already_debuffed_not_touched = initial_prob * (1.0 - prob_touched); @@ -1968,17 +1969,19 @@ void one_strike_fight(const battle_context_unit_stats& stats, // If we were killed in an earlier fight, we don't get to attack. // (Most likely case: we are a first striking defender subject to a series // of attacks.) - const double alive_prob = hp_dist.empty() ? 1.0 : 1.0 - hp_dist[0]; + double alive_prob = hp_dist.empty() ? 1.0 : 1.0 - hp_dist[0]; + if(stats.hp == 0) { + alive_prob = 0.0; + } const double hit_chance = (stats.chance_to_hit / 100.0) * alive_prob; if(opp_hp_dist.empty()) { opp_hp_dist = std::vector(opp_stats.max_hp + 1); - if(strikes == 1) { + if(strikes == 1 && opp_stats.hp > 0) { opp_hp_dist[opp_stats.hp] = 1.0 - hit_chance; opp_hp_dist[std::max(opp_stats.hp - stats.damage, 0)] = hit_chance; opp_not_hit *= 1.0 - hit_chance; } else { - assert(strikes == 0); opp_hp_dist[opp_stats.hp] = 1.0; } } else { @@ -1994,17 +1997,16 @@ void one_strike_fight(const battle_context_unit_stats& stats, } // If we killed opponent, it won't attack us. - const double opp_alive_prob = 1.0 - opp_hp_dist[0] / alive_prob; - const double opp_hit_chance = (opp_stats.chance_to_hit / 100.0) * opp_alive_prob; + const double opp_attack_prob = (1.0 - opp_hp_dist[0]) * alive_prob; + const double opp_hit_chance = (opp_stats.chance_to_hit / 100.0) * opp_attack_prob; if(hp_dist.empty()) { hp_dist = std::vector(stats.max_hp + 1); - if(opp_strikes == 1) { + if(opp_strikes == 1 && stats.hp > 0) { hp_dist[stats.hp] = 1.0 - opp_hit_chance; hp_dist[std::max(stats.hp - opp_stats.damage, 0)] = opp_hit_chance; self_not_hit *= 1.0 - opp_hit_chance; } else { - assert(opp_strikes == 0); hp_dist[stats.hp] = 1.0; } } else {