v0.8.9 (Bugfixes + translations) released to Google Play!

Useful links
Source code of the game - Contribution guide - ATCS Editor - Translate the game on Weblate - Example walkthrough - Andor's Trail Directory - Join the Discord
Get the game (v0.8.9) from Google, F-Droid, our server, or itch.io

Miscalculations in enemy difficulty

A place to submit bugs to the Andor's Trail Development Team.
QQkp
Posts: 65
Joined: Sat Dec 21, 2019 4:33 am
android_version: 10 - Android 10

Miscalculations in enemy difficulty

Post 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
Gonk
Posts: 192
Joined: Mon Mar 04, 2019 8:45 pm
android_version: 8.1 - Oreo

Re: Miscalculations in enemy difficulty

Post 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.
Gonk
Posts: 192
Joined: Mon Mar 04, 2019 8:45 pm
android_version: 8.1 - Oreo

Re: Miscalculations in enemy difficulty

Post 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.
User avatar
rijackson741
Posts: 4451
Joined: Tue Aug 20, 2013 2:04 am
android_version: 10 - Android 10
Location: Somewhere in Dhayavar
Contact:

Re: Miscalculations in enemy difficulty

Post 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.
You do not have the required permissions to view the files attached to this post.
Level:71, XP:6493739, PV:608, FQ:84
HP:210, AC:212, AD:58-77, AP:4, ECC:16%, CM:1.5, BC:188, DR:3
Gold: 237559 | RoLS:1, RoL:1, GoW:1, VSH:1, RoFLS:1, WoB:1
HH:1, WA:1, CS:2, Cl:1, IF:4, Ev:3, Re:2, WP:DA:1, WP:1S:1, WP:B:1, AP:L:1, FS:DW:2, S:DW:1
QQkp
Posts: 65
Joined: Sat Dec 21, 2019 4:33 am
android_version: 10 - Android 10

Re: Miscalculations in enemy difficulty

Post 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.
User avatar
rijackson741
Posts: 4451
Joined: Tue Aug 20, 2013 2:04 am
android_version: 10 - Android 10
Location: Somewhere in Dhayavar
Contact:

Re: Miscalculations in enemy difficulty

Post 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.
Level:71, XP:6493739, PV:608, FQ:84
HP:210, AC:212, AD:58-77, AP:4, ECC:16%, CM:1.5, BC:188, DR:3
Gold: 237559 | RoLS:1, RoL:1, GoW:1, VSH:1, RoFLS:1, WoB:1
HH:1, WA:1, CS:2, Cl:1, IF:4, Ev:3, Re:2, WP:DA:1, WP:1S:1, WP:B:1, AP:L:1, FS:DW:2, S:DW:1
H46732f
Posts: 20
Joined: Thu Nov 14, 2019 11:47 pm
android_version: 4.4 - Kitkat
Location: Brasil (Brazil)

Re: Miscalculations in enemy difficulty

Post 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
Last edited by H46732f on Sun Jan 05, 2020 10:18 am, edited 2 times in total.
QQkp
Posts: 65
Joined: Sat Dec 21, 2019 4:33 am
android_version: 10 - Android 10

Re: Miscalculations in enemy difficulty

Post 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.
H46732f
Posts: 20
Joined: Thu Nov 14, 2019 11:47 pm
android_version: 4.4 - Kitkat
Location: Brasil (Brazil)

Re: Miscalculations in enemy difficulty

Post 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
User avatar
rijackson741
Posts: 4451
Joined: Tue Aug 20, 2013 2:04 am
android_version: 10 - Android 10
Location: Somewhere in Dhayavar
Contact:

Re: Miscalculations in enemy difficulty

Post 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
You do not have the required permissions to view the files attached to this post.
Level:71, XP:6493739, PV:608, FQ:84
HP:210, AC:212, AD:58-77, AP:4, ECC:16%, CM:1.5, BC:188, DR:3
Gold: 237559 | RoLS:1, RoL:1, GoW:1, VSH:1, RoFLS:1, WoB:1
HH:1, WA:1, CS:2, Cl:1, IF:4, Ev:3, Re:2, WP:DA:1, WP:1S:1, WP:B:1, AP:L:1, FS:DW:2, S:DW:1
Post Reply