Skip to content

Commit

Permalink
Fix wesnoth#3042: attack prediction gives wrong results for HP <= 0 u…
Browse files Browse the repository at this point in the history
…nits

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.
  • Loading branch information
jyrkive committed May 28, 2018
1 parent 0200487 commit 472b0cb
Showing 1 changed file with 10 additions and 8 deletions.
18 changes: 10 additions & 8 deletions src/attack_prediction.cpp
Expand Up @@ -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);
Expand Down Expand Up @@ -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<double>(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<int>(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 {
Expand All @@ -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<double>(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<int>(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 {
Expand Down

0 comments on commit 472b0cb

Please sign in to comment.