The power of fractions

A rounding error

image Imagine that you have a city defended by a strong army: a hundred spearmen, thirty templars in good armor, twenty knights. Then out of nowhere an attacker rides up with three militia. The cheapest unit in the game, the kind you train in thirty seconds from a pile of wheat.

Three militia should not be able to do anything to that army. In lore, they would not get within spear's reach. In the numbers, they deal about one percent of the damage needed to bring down a single armored knight. And yet, until our recent patch, those three militia would reliably kill one spearman, one templar, and one knight every single time they attacked. They would die doing it, but they would take a knight with them.

Even worse is that the exploit scaled perfectly. Sixty militia split into twenty separate three-militia attacks would delete twenty spearmen, twenty templars, and twenty knights, gear and all. Sixty militia is barely noticeable in cost, but twenty geared knights is worth a fortune. In this blog post i wanted to talk about what happened.

Where the damage went wrong

image

When two armies clash, the engine works out how much damage each side deals, then spreads that damage across the enemy army. Lightly armored units soak up more of it, heavily armored units soak up less. So far so good.

The bug lived in the very last step that turns damage into bodies. The code asked, for each unit type, "how many of these did the incoming damage kill?" and it rounded that number up.

Rounding up maybe sounds harmless, but it is not. Rounding up means that any amount of damage above zero, no matter how small, becomes at least one dead unit. Three militia throwing their noodle arm punch at a wall of knights still produced a fractional kill of something like 0.01 knights, and rounding up turned that 0.01 into a whole dead knight. The number of casualties was being driven by how many different unit types the defender fielded, not by how much damage the attacker actually dealt.

The fix: make damage actually mean something

image

The principle we settled on is that casualties should be damage-conserving. If you deal enough damage to kill two units, you kill two units. If you deal enough to kill two and a half, you kill two for certain and you get a coin flip, weighted to your favor, for the third. And if you deal a trivial sliver of damage against something well armored, you kill nothing, because a sliver is not a kill.

Concretely, we changed three things in how a stack takes losses:

  1. Whole kills always land. Deal damage worth three dead spearmen, and three spearmen die, every time. Legitimate armies lose nothing to rounding.
  2. The leftover becomes a chance, not a freebie. The fractional remainder is now a probability of one extra kill, so over many fights the casualties average out to exactly the damage dealt.
  3. A threshold protects armor from chip damage. That leftover only gets to roll for a kill if it is at least ten percent of the unit's health. Three militia against an armored knight produce a remainder far below that line, so it deals nothing at all. They literally cannot scratch the knight.

The result lines up with both the math and the fiction. A swarm of cheap troops can still wear down other cheap troops, which is what swarms are for. But they bounce off heavy armor, which is what heavy armor is for. To grind a defender's elite units down, you now have to bring enough force to deal real damage to them.

What this means at the table

If you have been defending with a mix of cheap line troops and expensive armored elites, your elites just got noticeably harder to pick apart. Gear matters more than it did yesterday, because the armor on your knights and templars now does exactly what it promises: it makes them hard to kill, instead of being quietly ignored by a rounding rule.

If you were the one sending token attacks to farm someone's knights, that strategy is retired. Sorry. It was never supposed to work.

Combat now uses a small amount of controlled randomness in exactly one place, the leftover fraction of a kill. Everything up to that point is deterministic, and the randomness is calibrated so that across many battles the losses converge on the true damage. You will not notice it in a big fight.

If you spot something that feels off in the numbers, tell us. Half the time it is intended. The other half, it is a Math.ceil waiting to be caught.

← Back to all posts