Page 1 of 3

Miscalculations in enemy difficulty

Posted: Fri Jan 03, 2020 10:25 pm
by QQkp
Encountered this earlier today on 0.7.7. Currently doing Colonel Lutarc, round 1 against a lizard. The lizard's difficulty is listed as "Very hard", but the battle was actually fairly easy. Stats:

Lizard: 100 HP, 10 AP, 4 attack cost, 40 AC, 1-4 damage, 120 BC, 5 DR
My character: 37 HP, 10 AP, 3 attack cost, 132 AC, 19-24 damage, 31 BC, 1 DR

Since the AC-to-BC difference is almost equal, the two actors land hits about equally often. Each hit of mine does around 16.5 damage, so the lizard dies in about 6 hits. Each hit from the lizard does around 1.5 damage, so it would likely take over 20 hits for my character to die. My character also gets more attacks per turn, so it's a pretty straightforward win.

The game appears to be assigning a difficulty of "Very hard" to this match-up due to a bug in CombatController::getAverageDamagePerHit. In particular, DR is taken into account after scaling damage based on the chance of hitting, effectively applying DR to misses.

There are a couple other errors of note in the same function:
  • DR is applied to the average damage, which doesn't account for scenarios where the low end of the damage range would be negative
  • Critical hits are added to the average damage as if they happen in addition to a normal hit, rather than replacing a normal hit (effectively increasing the critical multiplier by 100%)
The exact correct logic is tricky - it's hard to account for scenarios where you roll low damage, get a critical hit, and end up dealing a small amount of damage after accounting for the enemy's DR. However, I think this should at least get the logic mostly correct:

Code: Select all

	private static float getAverageDamagePerHit(Actor attacker, Actor target) {
		Range scaledDamage = new Range(attacker.getDamagePotential());
		scaledDamage.subtract(target.getDamageResistance(), false);
		float result = scaledDamage.averagef();
		if (hasCriticalAttack(attacker, target)) {
			result += (float) attacker.getEffectiveCriticalChance() * result * (attacker.getCriticalMultiplier() - 1) / 100;
		}
		result = (float) (getAttackHitChance(attacker, target)) * result / 100;
		return result;
	}
Sorry, I haven't tested this yet. I haven't been home lately, so I have no build environment set up yet, or else I'd be offering this as a pull request. The changes are:
  • Delay applying hit chance until the end, since a miss does 0 damage no matter what
  • Apply DR to the damage potential before taking the average to let the Range util class handle underflow
  • Subtract 1 from the crit multiplier to avoid overcounting crit damage

Re: Miscalculations in enemy difficulty

Posted: Sat Jan 04, 2020 9:20 am
by Gonk
Thank you for finding and analyzing this. Did you verify that the fix would change the resulting difficulty to a more adequate value?

I will take a look at this, too, but it may take some time. Would be great if you could do a pull request.

Re: Miscalculations in enemy difficulty

Posted: Sat Jan 04, 2020 10:02 pm
by Gonk
I did a quick look at the code. Thanks again. Your suggestions to delay the hit chance and reduce the crit multiplier by 1 make sense.
But I think the DR should be substracted after the crits were applied.

Re: Miscalculations in enemy difficulty

Posted: Sat Jan 04, 2020 10:19 pm
by rijackson741
I haven't looked at the code, but the math should look like this:
Average damage.png
Divide HP by average damage per round, and you have the average number of rounds needed to kill your opponent, or to get killed.

Re: Miscalculations in enemy difficulty

Posted: Sat Jan 04, 2020 10:57 pm
by QQkp
Gonk: Yes, DR should be applied after crit. It gets tricky because the Range util class assumes an integer range with a step size of 1, but crits stretch the step size of the range. If your character has a normal damage potential of 1 to 2 and a 2x critical, your criticals do either 2 or 4 damage - but never 3. (Edit: By the way, to your first question - no, I haven't tested the code at all. I've been traveling a bit for holidays, so I haven't set up the build environment yet - sorry about that.)

rijackson741: The formulas for d_nc and d_c look a little off to me - averaging the min and max damage to get the range's average hinges on the assumption that the damage output increases linearly over the range, but bounding the damage at 0 kills that assumption.

Quick example, consider this scenario:
HC = 1 (100% hit rate, so D = d)
ECC = 0; CM = 1 (no crits, so d = d_nc)
AD_min = 1
AD_max = 10
DR = 9

In game, that means you're essentially rolling a d10 - if it comes up 10, you do 1 damage; if it comes up with anything else, you get 0. Logically, average damage output should be 0.1. But by the formula above:

d_nc = (max(0, 1 - 9) + max(0, 10 - 9)) / 2 = (0 + 1) / 2 = 0.5

Amusingly, I think my suggested code above suffers from the same glitch. It's a surprisingly tricky little thing.

Re: Miscalculations in enemy difficulty

Posted: Sun Jan 05, 2020 12:32 am
by rijackson741
Yes, you are right. Bounding the damage at 0 may mean there is no simple formula. If there is, it's not obvious to me what that would be, but I'll think about it.

We cannot ignore the case where DR>AD either. You can get enough DR early in the game that some early monsters cannot cannot hurt you. So as long as you can do some damage to them, they have to be flagged as very easy. Even if your ECC is only 1%, and you can only do damage with a critical hit! You can't be killed, so it's just a game of patience.
Less likely, but not impossible, is that you bump into a monster with a DR that is higher than your minimum AD. In which case how hard it is to kill will depend very much on what DR it has, relative to your AD. It's theoretically possible that you can't kill it at all, although it's very unlikely a player will find themselves in that situation.

And then, to open a really big can of worms, these calculations do not take into a account the conditions a monster can inflict on you, or vice versa. Or your, or the monsters, ability to heal HP during a fight. Or whether, for example, you have Cleave, that gives you an extra turn in some fights. Or whether you have Evasion skill, and can flee when the going gets too rough. Or very many other things, for which there are certainly no simple formulas.

Re: Miscalculations in enemy difficulty

Posted: Sun Jan 05, 2020 1:20 am
by H46732f
For specifically the case of DR, just do in common hits:
if AD_min <= DR
{
newAD_max = AD_max - DR;
d_nc =0;
if( newAD_max>=0)
d_nc = (newAD_max ?/* termial */) / ( AD_max - AD_min + 1)

}

and in critical hits:
if AD_min * CM <= DR
{
newAD_max = AD_max*CM - DR;
d_c =0;
if( newAD_max>=0)
d_c = ((newAD_max/CM) ?)*CM) / ( AD_max - AD_min + 1)
}


Termial: n? = (n^2 + n) / 2

Re: Miscalculations in enemy difficulty

Posted: Sun Jan 05, 2020 5:38 am
by QQkp
Makes sense for normal hits as long as we correctly catch cases where newAD_max < 0.

For crits, I think there's still a minor issue around the case that rijackson741 pointed out, where you can only damage an enemy with a crit. Here's a case:
AD_min = 1
AD_max = 2
CM = 2
DR = 3

Here, to damage the enemy, you must roll a 2 for damage and get a crit to overcome DR - so d_c should be 0.5. But newAD_max is only 1. If we're doing integer division, newAD_max/CM floors to 0. If we allow float, it looks like the formula works out to 0.375.

It might still be close enough for the purpose of difficulty calculation, though. I'm guessing accounting for conditions will always remain out of scope (it's much more complex, and probably isn't worth the upkeep since any future conditions would also have to be added). And fractional crit multipliers throw in another little wrinkle, since damage values are always cast back to int.

Re: Miscalculations in enemy difficulty

Posted: Sun Jan 05, 2020 10:18 am
by H46732f
QQkp wrote: Sun Jan 05, 2020 5:38 am Makes sense for normal hits as long as we correctly catch cases where newAD_max < 0.

For crits, I think there's still a minor issue around the case that rijackson741 pointed out, where you can only damage an enemy with a crit. Here's a case:
AD_min = 1
AD_max = 2
CM = 2
DR = 3

Here, to damage the enemy, you must roll a 2 for damage and get a crit to overcome DR - so d_c should be 0.5. But newAD_max is only 1. If we're doing integer division, newAD_max/CM floors to 0. If we allow float, it looks like the formula works out to 0.375.

It might still be close enough for the purpose of difficulty calculation, though. I'm guessing accounting for conditions will always remain out of scope (it's much more complex, and probably isn't worth the upkeep since any future conditions would also have to be added). And fractional crit multipliers throw in another little wrinkle, since damage values are always cast back to int.
I already edited my post to handle the case of newAD_max <= 0;

As for the small mistake in the case of critics I think it would only be completely solved by making a very complex iteration structure (for (;;)) that I don't think is necessary.
Rounding can really interfere, but I think it's an improvement anyway if I'm not forgetting anything

Re: Miscalculations in enemy difficulty

Posted: Sun Jan 05, 2020 2:16 pm
by rijackson741
This is correct (I put a lot more thought into it this time, and checked the formulas with some examples)
Average damage.png