I think that this might run fast enough to be used in "get best defender".
Maybe it won't. But at least it should run much faster than the current binomial theorem based "chance to win" calculator.
It generates a fixed point answer to the question "what fraction of your HP would you expect to lose beating the target, and failing that, what fraction of their HP would you expect to chip away" in a quick, approximate way.
Higher values are better.
Someone else is in charge of generating "vs Melee" multipliers on the power of each combatant -- they are expected to be already factored in.
CombatChance and AttackPower must not be greater than MaxQuickCombatPower. (I should add checks...)
Note that MaxQuickCombatPower is 2^15, or about 32000. The limit on those two values is mainly if you want to scale them up for rounding purposes.
AttackPower is "damage you do per hit", scaled if you want. CombatChance is the number that is used to determine if you win a given combat round.
This code has not been compiled nor tested, it was just written as-is. Some work was put into making sure that the fixed point values don't break the 2^31-1 barrier of ints.
Math was done in fixed point out of an irrational, and probably unwarranted, fear of doubles. Probably doing it in floating point would be a better idea. Then again, who knows what the civ (graphics) engine has done to the FPU precision!
Maybe it won't. But at least it should run much faster than the current binomial theorem based "chance to win" calculator.
It generates a fixed point answer to the question "what fraction of your HP would you expect to lose beating the target, and failing that, what fraction of their HP would you expect to chip away" in a quick, approximate way.
Higher values are better.
Someone else is in charge of generating "vs Melee" multipliers on the power of each combatant -- they are expected to be already factored in.
CombatChance and AttackPower must not be greater than MaxQuickCombatPower. (I should add checks...)
Note that MaxQuickCombatPower is 2^15, or about 32000. The limit on those two values is mainly if you want to scale them up for rounding purposes.
AttackPower is "damage you do per hit", scaled if you want. CombatChance is the number that is used to determine if you win a given combat round.
Code:
// this should take into account the changes to power
// due to the other side's unit type, and the terrain type
// you are on, etc. It shouldn't take into account combat
// strength changes due to Health changes:
struct QuickCombatSide {
int CombatChance; // assumes A/(A+B) chance to hit
int AttackDamage; // damage done per attack
int Health; // your current health
int FirstStrikes; // # of first strikes
int ChanceFirstStrikes; // # of first strike chances
};
// this first calculates "in a typical case, how much damage
// would an immortal version of myself take to kill the enemy".
// It then rescales that by this unit's Health, and up by 100,
// generating a sort of "what fraction of myself does it take
// to kill this target".
// Keeping the scales small enough that MaxQuickCombatScale squared
// isn't a problem
enum {
QuickCombatScale = 1<<8, // 256
MaxQuickCombatPower = 1<<7*QuickCombatScale, // 128
MaxHealth = 100, // note, it is important that MaxQuickCombatPower >= MaxHealth
};
int DamageRatioScaled( QuickCombatSide you, QuickCombatSide other);
// This returns a fixed point value, where QuickCombatScale is 1.0
//
int QuickCombatPower(
QuickCombatSide you,
QuickCombatSide other
)
{
if (you.Health <= 0) { return 0; }
if (other.Health <= 0) { return MaxQuickCombatPower; }
if (you.Health > MaxHealth) { you.Health = MaxHealth; }
if (other.Health > MaxHealth) { other.Health = MaxHealth; }
// Expected damage from first strikes, scaled by QuickCombatScale
int FirstStrikeDamageScaled = 0;
{
int FirstStrikesTimesTwo =
you.FirstStrikes*2 + you.ChanceFirstStrikes -
other.FirstStrikes*2 - other.ChanceFirstStrikes;
if (FirstStrikesTimesTwo > 0) {
int FirstStrikeHitsScaledPerRound = (you.CombatChance*QuickCombatScale)/(you.CombatChance+other.CombatChance);
int FirstStrikeDamageScaledPerRound = FirstStrikeHitsPerRound * you.AttackDamage;
FirstStrikeDamageScaled = (FirstStrikeDamageScaledPerRound * FirstStrikesTimesTwo+1) / 2;
} else {
int FirstStrikeHitsScaledPerRound = (other.CombatChance*QuickCombatScale)/(you.CombatChance+other.CombatChance);
int FirstStrikeDamageScaledPerRound = FirstStrikeHitsPerRound * other.AttackDamage;
FirstStrikeDamageScaled = (FirstStrikeDamageScaledPerRound * FirstStrikesTimesTwo+1) / 2;
}
}
int otherFirstStrikeDamageScaled = 0;
int yourFirstStrikeDamageScaled = 0;
if (FirstStrikeDamageScaled > 0) {
otherFirstStrikeWoundsScaled = +FirstStrikeDamageScaled;
} else {
yourFirstStrikeWoundsScaled = -FirstStrikeDamageScaled;
}
// the 100 here is because you.Health is a percentage
int healthYouLostToFirstStrikesScaled = (you.Health*QuickCombatScale - yourHealthScaled)*100 / you.Health;
// are you likely to lose in the first strikes? Sucks to be you.
if (you.Health*QuickCombatScale <= yourFirstStrikeWoundsScaled) return 0;
// are you likely to win in the first strikes? Rocks to be you
if (other.Health*QuickCombatScale <= otherFirstStrikeWoundsScaled) return MaxQuickCombatPower;
// Next, work out the damage ratio. This is the ratio between the average damage
// and the average damage they do to you, per round.
// 1.0 means "every point you do, they do a point, you are even"
// > 1.0 means you are ahead.
int damageRatioScaled = DamageRatioScaled(you, other);
if (damageRatioScaled <= 0) {
// you aren't expected to do any damage. This isn't good.
// however, you might have done some damage in the earlier part.
int fracHealthTheyLostToFirstStrikesScaled = otherFirstStrikeWoundsScaled / other.Health;
// the 100 above here is because other.Health is a percentage
if (fracHealthTheyLostToFirstStrikesScaled <= 0) {
return 0;
} else {
return fracHealthTheyLostToFirstStrikesScaled;
}
}
int expectedDamageTakenScaledPostFirstStrike = (other.Health*QuickCombatScale - otherFirstStrikeWoundsScaled)*QuickCombatScale / damageRatioScaled;
int expectedDamageTakenToWinScaled = expectedDamageTakenScaledPostFirstStrike + yourFirstStrikeWoundsScaled;
// if you aren't expected to take any damage...
if (expectedDamageTakenScaled <= 0) {
return MaxQuickCombatPower;
}
// smaller fractions of yous to win is better!
int fractionOfYousToWinScaled = expectedDamageTakenToWinScaled*100 / you.Health;
if (fractionOfYousToWinScaled <= 0) { return MaxQuickCombatPower; }
// The return value, bigger is more powerful, hence better:
int retvalScaled = QuickCombatScale * QuickCombatScale / fractionOfYousToWinScaled;
if (retvalScaled > MaxQuickCombatPower) return MaxQuickCombatPower;
return retvalScaled;
}
int DamageRatioScaled( QuickCombatSide you, QuickCombatSide other)
{
// catch blowthrough:
if (you.AttackDamage > other.Health) you.AttackDamage = other.Health;
if (other.AttackDamage > you.Health) other.AttackDamage = you.Health;
if (other.CombatChance <= 0) return MaxQuickCombatPower;
int HitRatioScaled = you.CombatChance * QuickCombatScale / other.CombatChance;
if (HitRatioScaled > MaxQuickCombatPower) HitRatioScaled = MaxQuickCombatPower;
if (other.AttackDamage <= 0) return MaxQuickCombatPower;
int DamageRatioScaled = you.AttackDamage * QuickCombatScale / other.AttackDamage;
if (DamageRatioScaled > MaxQuickCombatPower) DamageRatioScaled = MaxQuickCombatPower;
int retvalScaled = HitRatioScaled * DamageRatioScaled / QuickCombatScale;
if (retvalScaled > MaxQuickCombatPower) return MaxQuickCombatPower;
return retvalScaled;
}
Math was done in fixed point out of an irrational, and probably unwarranted, fear of doubles. Probably doing it in floating point would be a better idea. Then again, who knows what the civ (graphics) engine has done to the FPU precision!