We'll begin with a few assumptions:
+ Show Spoiler [Assumptions] +
-Units have the following properties: range (R), damage per second (DPS), collision radius (CR), hit points (HP), and movement speed (MS).
-All units deal single-target damage (AoE is situational and I don't know how to systematically model it).
-Units are packed tightly with collision radii tight against each other.
-No overkill (specific unit-vs-unit models could account for some overkill, but for a general analysis I won't be dealing with it).
-Engaging units are on attack-move commands towards each other.
-Units killed in the front line of an arc are immediately replaced by units from behind if a replacement is available.
-Armies are composed of a single kind of unit. This analysis has implications for how multi-unit armies would function, but does not directly calculate them.
-All units deal single-target damage (AoE is situational and I don't know how to systematically model it).
-Units are packed tightly with collision radii tight against each other.
-No overkill (specific unit-vs-unit models could account for some overkill, but for a general analysis I won't be dealing with it).
-Engaging units are on attack-move commands towards each other.
-Units killed in the front line of an arc are immediately replaced by units from behind if a replacement is available.
-Armies are composed of a single kind of unit. This analysis has implications for how multi-unit armies would function, but does not directly calculate them.
What I want is a way to express mathematically how an engagement between two armies will go, who will win, and by how much. This is easiest in single unit vs. single unit calculations. These are simple and only tangentially related to the problem at hand, so I'll put them in a spoiler. If you have trouble following the equations, I apologize that I am writing everything out in a text format, with no subscripts, fraction bars, etc.
+ Show Spoiler [Single Unit vs. Single Unit] +
Let's suppose we have two units:
-Unit A has quantities DPS(A), HP(A), R(A), and MS(A).
-Unit B has quantities DPS(B), HP(B), R(B), and MS(B).
The rate at which Unit A deals damage is equal to DPS(A), which means total damage done by Unit A, a quantity I'll call D(A), is equal to DPS(A) times time. When D(A) = HP(B) unit B is dead. The same is true for Unit B with the respective values; D(B) = DPS(B) * t, and when D(B) = HP(A), unit A is dead.
So if both units begin the fight within range of each other, then solving DPS(A) * t(A) = HP(B) will give t(A), or the amount of time in seconds it takes Unit A to kill Unit B. DPS(B) * t(B) = HP(A) will give t(B), the amount of time in seconds it takes Unit B to kill Unit A. If t(A) < t(B), Unit A will win the fight; if t(A) > t(B), then Unit B will win. Assuming Unit A wins the fight, the total damage taken by Unit A = DPS(B) * t(A); leftover HP on Unit A = HP(A) – DPS(B) * t(A).
If Unit A has greater range than Unit B, and the units approach each other from a distance, then there is a brief period in which Unit A is firing and Unit B is not. In other words, Unit A begins firing at t=0, while Unit B begins firing at t = the time required to traverse the difference in ranges. That time is given by t = [R(A) – R(B)] / MS(B). In this case:
-Unit A has quantities DPS(A), HP(A), R(A), and MS(A).
-Unit B has quantities DPS(B), HP(B), R(B), and MS(B).
The rate at which Unit A deals damage is equal to DPS(A), which means total damage done by Unit A, a quantity I'll call D(A), is equal to DPS(A) times time. When D(A) = HP(B) unit B is dead. The same is true for Unit B with the respective values; D(B) = DPS(B) * t, and when D(B) = HP(A), unit A is dead.
So if both units begin the fight within range of each other, then solving DPS(A) * t(A) = HP(B) will give t(A), or the amount of time in seconds it takes Unit A to kill Unit B. DPS(B) * t(B) = HP(A) will give t(B), the amount of time in seconds it takes Unit B to kill Unit A. If t(A) < t(B), Unit A will win the fight; if t(A) > t(B), then Unit B will win. Assuming Unit A wins the fight, the total damage taken by Unit A = DPS(B) * t(A); leftover HP on Unit A = HP(A) – DPS(B) * t(A).
If Unit A has greater range than Unit B, and the units approach each other from a distance, then there is a brief period in which Unit A is firing and Unit B is not. In other words, Unit A begins firing at t=0, while Unit B begins firing at t = the time required to traverse the difference in ranges. That time is given by t = [R(A) – R(B)] / MS(B). In this case:
D(A) = DPS(A) * t
D(B) = DPS(B) * (t – [R(A) – R(B)] / MS(B))
t(A) = HP(B) / DPS(A)
t(B) = [HP(A) / DPS(B)] + [R(A) – R(B) / MS(B)]
if A is the victor:
damage taken = D(B) = DPS(B) * (t(A) – [R(A) – R(B)] / MS(B))
leftover HP = HP(A) – D(B)
if B is the victor:
damage taken = D(A) = DPS(A) * t(B)
leftover HP = HP(B) – D(A)
D(B) = DPS(B) * (t – [R(A) – R(B)] / MS(B))
t(A) = HP(B) / DPS(A)
t(B) = [HP(A) / DPS(B)] + [R(A) – R(B) / MS(B)]
if A is the victor:
damage taken = D(B) = DPS(B) * (t(A) – [R(A) – R(B)] / MS(B))
leftover HP = HP(A) – D(B)
if B is the victor:
damage taken = D(A) = DPS(A) * t(B)
leftover HP = HP(B) – D(A)
Army engagements are more complex. When two armies engage, not all of the units are necessarily firing; instead, they form arcs of units that oppose each other, and units in the front row of the arc are firing. Once a unit has approached the enemy closely enough to fire at the enemy they stop moving, so units behind them are stuck and cannot fire unless they run around the unit in front of them, thus widening the arc. The widest the unit arc can be is the width of the area they are engaging in. So if two armies engage in a narrow passage which is 5 wide, the largest the arc can be is 5. In calculations I will write arc length as AL.
Note that attack range begins at the edge of a unit's collision radius, not the center of the circle which they occupy.
In the case of units with equal range engaging each other, as for instance an engagement between two armies of marines, the front row and only the front row is firing. This means that if the arc lengths are equal, even if one army is bigger than the other, the trade will still be even. So if 20 marines engage 200 marines in a choke so that neither side has a bigger arc, then at the end of the battle both sides should have lost about 20 marines.
This is approximately the structure of marine vs. marine fights.
If one army has more range than the other, though, then that army can have more than just the front row of units firing while the opposing army only has the front row firing.
This would roughly represent, for instance, roaches firing at marines
The larger the range difference, the more it increases the number of units firing. It would seem from the pictures above that the range difference would have to be equal to 2 collision radii in order to increase the rows firing by one. In fact, this isn't quite accurate; while I did draw it this way, arranging units in rows and columns at right angles to each other is not the most tightly packed formation; the more accurate picture would have each row offset from the other one to take advantage of the space between circles, so the real spacing between rows is approximately sqrt(3) times the collision radius.
This is a more accurate representation of the arrangement of units in a clump.
From this, we can calculate an estimate of how many units will be firing in an engagement between two armies.
+ Show Spoiler [Calculations] +
Now we have an engagement between two armies:
-Army A, made up of one type of unit with quantities R(A) and CR(A); Arc Length = AL(A)
-Army B, made up of one type of unit with quantities R(B) and CR(B); Arc Length = AL(B)
The number of units is given by the width of the row divided by the width of each unit. So each row of A has AL(A) / 2 * CR(A) units in it, and each row of B has AL(B) / 2 * CR(B) unit in it.
Once again we will assume that Army A has greater range units. The range difference is given by R(A) – R(B), so the number of rows that are firing for Army A is given by R(A) – R(B) divided by the row spacing. So number of rows firing is equal to [R(A) – R(B)] / sqrt(3) * CR(A). Remember to always round this number up. For example, if the range difference is 0, there is one row firing. If the range difference is equal to one row spacing, there are two rows firing. The equation could alternatively be written as [R(A) – R(B)] / sqrt(3) * CR(A) + 1, in which case you would always round the number down.
The total units firing is given by the number of units per row times the number of rows firing. So the overall equation is:
Number of units firing = AL(A) * [R(A) – R(B)] / sqrt(3) * 2 * CR(A)^2. This is somewhat deceptive, since the number of rows firing must be an integer. But it is still worth noting that number of units firing is proportional to AL(A) * [R(A) – R(B)] / CR(A)^2.
-Army A, made up of one type of unit with quantities R(A) and CR(A); Arc Length = AL(A)
-Army B, made up of one type of unit with quantities R(B) and CR(B); Arc Length = AL(B)
The number of units is given by the width of the row divided by the width of each unit. So each row of A has AL(A) / 2 * CR(A) units in it, and each row of B has AL(B) / 2 * CR(B) unit in it.
Once again we will assume that Army A has greater range units. The range difference is given by R(A) – R(B), so the number of rows that are firing for Army A is given by R(A) – R(B) divided by the row spacing. So number of rows firing is equal to [R(A) – R(B)] / sqrt(3) * CR(A). Remember to always round this number up. For example, if the range difference is 0, there is one row firing. If the range difference is equal to one row spacing, there are two rows firing. The equation could alternatively be written as [R(A) – R(B)] / sqrt(3) * CR(A) + 1, in which case you would always round the number down.
The total units firing is given by the number of units per row times the number of rows firing. So the overall equation is:
Number of units firing = AL(A) * [R(A) – R(B)] / sqrt(3) * 2 * CR(A)^2. This is somewhat deceptive, since the number of rows firing must be an integer. But it is still worth noting that number of units firing is proportional to AL(A) * [R(A) – R(B)] / CR(A)^2.
The conclusion of our calculations was:
Number of units firing is proportional to arc length * range advantage / collision radius squared.
The number of units firing is important when considering how a certain type of unit "scales," or gets better in bigger numbers. If all your units are firing at once, you can do damage faster than an opponent that only has some of their units firing at once. So units scale better with range, and scale worse with collision radius squared. If your whole army isn't firing, you can help fix this by spreading them in a wider arc.
Now that we can calculate how many units are firing in each army, we can calculate the results of two homogeneous armies engaging one another. Let's do a concrete example: suppose 50 marines without stim or combat shield engage 25 roaches, with no upgrades on either side. As before, calculations will be in a spoiler. As before, I apologize for having to write down equations in text format, without fraction bars, subscripts, etc.
+ Show Spoiler [More Calculations] +
50 Marines
HP: 45
DPS: 5.81 (Ordinarily 6.97, but roaches start with 1 armor).
Range: 5
Collision Radius: 0.375
Total HP: 2250
25 Roaches
HP: 145
DPS: 8.00
Range: 4
Collision Radius: 0.625
The damage rate of the marines is equal to number of marines firing * DPS of marines. We'll assume the arc length of the roaches and marines is the same, so we don't need to specify it and can draw a general conclusion. We'll combine this with our other constants to make k, some constant that varies with arc length. Number of marine rows firing equals:
[R(marines) – R(roaches)] / sqrt(3) * CR (marines) = [5 – 4] / 1.732 * .375 = 1.54. Since we always round this number up, we get that 2 rows of marines are firing, while 1 row of roaches is firing.
The number of marines per row is equal to AL / 2 CR(marines) = 1.333 * AL. The number of roaches per row is equal to AL / 2 CR(roaches) = .8 * AL.
This means there are 2.66 * AL marines firing at once since there are two rows, while there are .8 * AL roaches firing from their one row.
Damage rate of marines = DPS(marines) * number of marines firing = 15.45 * AL.
Damage rate of roaches = DPS(roaches) * number of roaches firing = 6.4 * AL.
In other words, for every 45 damage the roaches do, the marines do 108 damage. for every marine that is killed, 3/4 of a roach is killed. Or for every 4 marines that are killed, 3 roaches are killed.
Remember that these calculations only work when both sides are in sufficient numbers that the maximum possible marines and roaches are firing at any given time. So this will apply until there aren't enough marines to fill 2 rows, or there aren't enough roaches to fill one row. This requires us to specify an arc length. Lets choose an arc length of around 3.5, since this is just enough to fit 9 marines or 5 roaches in a row. In this case when we reach 18 marines or 5 roaches, we no longer have enough to fill a row. This happens first to the roaches, at which point 20 roaches and about 27 marines have been killed.
Now we have a range of values. At one end, the Terran focus fires perfectly, reducing the damage rate of the Zerg. At the other end, the terran does the perfect anti-focus fire, the roaches all stay alive until the exact last moment, and then die simultaneously. In the latter scenario, the zerg maintains his prior attack rate until the end, and about 33 marines are dead at the end of the fight; 17 remain. In the former scenario, there are a few extra marines alive as a result of good focus fire from the Terran.
So when 50 unupgraded marines engage 25 unupgraded roaches, there should be at least 17 marines alive at the end. Better focus fire from the terran will raise this number, but not higher than 22 marines. A smaller arc length will aid the zerg, pushing the number closer to 17 marines. Of course, this calculation also doesn't account for overkill, to which roaches are prone. Overkill might leave the Terran with much more than 17 marines at the end of the day.
The result of the calculations: not accounting for overkill from the roaches, then at least 17 marines should be alive at the end of the fight. Engaging in smaller spaces in this case helps the Zerg.
From this analysis we can draw a few interesting conclusions, although they may not be especially new, and mostly are already well-known. I guess you could call this a tl;dr if you like.
Tl;dr:
-Getting big arcs is very important in getting strong engagements. In small armies where you haven't yet reached your maximum rate of units firing, this isn't so important, but as armies get bigger, large arc lengths become more important. Big arcs are more important for low-range units (thus why engaging in open spaces and against spread-out opponents is so important for zerglings, and for zerg in general).
-Knowing how units perform in a one-on-one engagement doesn't tell you how they perform in a 100-vs-100 engagement. In particular, one-one-one engagements don't emphasize range and collision radius as much as larger engagements do. Thus a zealot can beat a roach in a one-on-one fight (without micro, at least), but lots of zealots lose hard to lots of roaches.
-Range is a really, really important quantity on units. When the immortal went from range 5 to range 6, this was a much more massive change than it sounds like. Queen range jumping from 3 to 5 has all kinds of Terrans up in arms about balance.