SFC2 has two battle engines. Nearly every fight runs on the per unit engine: it builds a record for every single ship and every single defence on the field, then rolls dice shot by shot until the battle is over. Once a fight gets big enough that this stops being practical, SFC2 switches to a second engine that never tracks individual ships at all. Instead it works out, for each type of unit, how many of them end up in each band of damage. That second engine is the large battle algorithm.
Both engines play by the same rules. Those rules are on the Combat page. This page is about how the big one gets to the answer.
When it runs
Before the first round, SFC2 adds up every unit on both sides, ships and defences together. If that total is 3,000,000 or more, the battle runs on the large battle algorithm. Under that, it runs on the per unit engine.
The switch is automatic. Nothing in the game turns it on or off, and there is no setting for it. It works the same way for a real Attack and for a run in the in-game Calculator, so a simulation of a huge fleet is using the same code the real battle will use. The Battle Report does not say which engine ran.
One difference worth knowing if you use the Calculator. A normal sized battle is simulated three times and the Calculator shows you the worst of the three for the attacker. A battle over the threshold is simulated once, because the large battle algorithm barely varies from run to run.
Note that both sides count towards the total. A 2,000,000 ship fleet hitting a planet with 1,500,000 defences is a 3,500,000 unit battle and goes to the large battle algorithm, even though neither side on its own is close to the line.
Why there are two engines
The per unit engine allocates one record per unit before the battle starts, and then loops over every shot that gets fired. That is fine for a normal fight. It is not fine for millions of units, and it is impossible for the biggest NPC stacks, which run into the hundreds of millions.
Here is the same battle put through both engines in dev, 3,000,000 attacking units against 3,000,000 defending units, no tech on either side:
| Engine | Time to resolve | Result | Attacker losses |
|---|---|---|---|
| Per unit | 2.46 seconds | Draw after 6 rounds | 2,496,792 |
| Large battle algorithm | 0.012 seconds | Draw after 6 rounds | 2,482,717 |
Same winner, same round count, losses about half a percent apart, and it took roughly two hundred times less work to get there. The gap widens fast: the per unit engine slows down in step with the number of units, while the large battle algorithm mostly cares about how many types of unit are on the field. A 210,000,000 unit battle still resolves in about the same hundredth of a second.
What does not change
None of the core combat rules are relaxed for a big battle:
- Six rounds at most, and it stops early the moment one side has nothing left.
- Both sides fire before anything is removed, so a unit that dies this round still got its shot off.
- Shots are not aimed. Every shot lands on a random unit from everything the other side still has.
- A shot worth 1 percent or less of the target's full Shield Rating is absorbed for nothing.
- Shields come back in full at the start of every round. Hull damage never repairs.
- A unit that took hull damage this round rolls to be destroyed at the end of it, at a chance equal to the share of its hull it has lost, with no roll at all below 30 percent lost.
- Rapid Fire at rating N still earns a follow up shot (N - 1) / N of the time.
Damage buckets
The whole trick is that the algorithm never asks "how much hull does this ship have left". It asks "how many of these ships are in each band of damage". Each type of unit gets 101 buckets. Bucket 0 holds the untouched ones, bucket 100 holds the dead ones, and everything in between is one step of damage further down.
The size of one step is worked out once, from the unit's shield and hull:
step = (hull + shield) / 100
If the shield is smaller than that, the shield gets no buckets of its own: the step becomes hull / 100 and all 100 steps are hull. Otherwise the shield is given a whole number of steps and the step is resized so the shield fills them exactly:
shield steps = floor(shield / step) step = shield / shield steps hull steps = hull / step
So the buckets are not the same size from one unit to the next. Using base ratings with no tech:
| Unit | Shield | Hull | One step | Shield steps | Hull steps |
|---|---|---|---|---|---|
| Artemis Class Fighter | 10 | 400 | 5 | 2 | 80 |
| Poseidon Class Cruiser | 50 | 2,700 | 50 | 1 | 54 |
| Athena Class Battleship | 200 | 6,000 | 66.7 | 3 | 90 |
| Hades Class Battleship | 400 | 7,000 | 80 | 5 | 87.5 |
| Prometheus Class Destroyer | 500 | 11,000 | 125 | 4 | 88 |
| Missile Battery | 20 | 200 | 2.22 | 9 | 90 |
| Plasma Cannon | 300 | 10,000 | 150 | 2 | 66.7 |
| Large Decoy | 10,000 | 10,000 | 200 | 50 | 50 |
| Zeus Class | 50,000 | 900,000 | 10,000 | 5 | 90 |
The Large Decoy is the odd one out. Half its buckets are shield, which is another way of saying what it is for.
Working out the shots
Every group of attacking units fires once per unit per round, plus whatever Rapid Fire earns. The algorithm never rolls those chains one at a time. It works out how many shots the whole group will fire in one go.
First, the chance that any single shot earns a follow up. For each type of unit on the other side, with rapid fire rating r against it (r is 1 when there is no rapid fire):
c = sum of ((r - 1) / r) x (that unit's count / total enemy units)
Then:
repeat shots = 1 / (1 - c) - 1 shots = unit count x (1 + repeat shots)
That is the closed form of the chain: fire, roll, fire again, roll again, out to infinity. The result gets nudged by a small random amount and is never allowed to fall below one shot per unit. At these fleet sizes the nudge is tiny next to the total, which is why the large battle algorithm gives almost the same answer every time you run the same battle.
Spreading the shots
Shots are not aimed at anything. The algorithm divides the shot count by the total number of units on the other side and treats the result as the average number of hits every defending unit takes:
average hits per defending unit = shots / total enemy units
Every type of unit on the other side gets the same average. That is what "random targets" means once you stop rolling dice. From that average a Poisson distribution gives the share of units hit 0 times, 1 time, 2 times, and so on.
Each hit moves a unit down a fixed number of buckets:
buckets per hit = attacker's Weapons Rating / defender's step
A Prometheus firing at a Hades does 2,000 damage against a step of 80, so it moves the Hades 25 buckets. An Artemis firing at the same Hades does 50 against a step of 80, or 0.625 of a bucket, and the algorithm splits that between the two nearest buckets in proportion. Anything that runs off the end lands in bucket 100 and is dead.
Before any of that, the bounce check runs. If a shot is worth 1 percent or less of the defender's full Shield Rating, that whole group of shots is thrown out against that type of unit and nothing happens at all.
Shields
All of the round's incoming damage is composed into one histogram first. Then the whole histogram is shifted back down by the defender's shield steps. That shift is what makes shields refill each round: any damage that only ever got as far as the shield is written off, and the shield absorbs up to its full rating once per round no matter how many shots arrived.
Piercing attacks are applied after the shift so that they skip the shield. Nothing in SFC2 pierces, so that second pass never does anything.
Blowing up
At the end of the round, every bucket that took hits rolls for destruction. The chance is the bucket index divided by the unit's hull steps, which is simply the share of hull that has been lost:
chance = bucket / hull steps
Below 0.3 nothing happens. At 1 or above, everything in that bucket that was hit is gone. In between, a random share around that chance is destroyed. Buckets that took no hits this round are safe even if they are badly damaged, which is the same rule the per unit engine follows.
Whatever survives stays in its bucket and carries into the next round.
A worked example
1,000,000 Prometheus and 2,000,000 Artemis attacking 1,000,000 Hades, 1,000,000 Athena and 1,000,000 Poseidon, no tech on either side. That is 6,000,000 units, so it runs on the large battle algorithm.
Round one, the Prometheus group. The only defender it has Rapid Fire against is the Hades, at x2, and the Hades are a third of the field:
c = (2 - 1) / 2 x (1,000,000 / 3,000,000) = 0.1667 repeat shots = 1 / (1 - 0.1667) - 1 = 0.2 shots = 1,000,000 x 1.2 = 1,200,000
The Artemis have no rapid fire against any of those three, so c is 0 and they fire 2,000,000 shots. That is 3,200,000 shots into 3,000,000 units, an average of 1.07 hits per defending unit for the round.
Then the damage. A Prometheus shot is 25 buckets on a Hades, 30 on an Athena and 40 on a Poseidon. An Artemis shot is 0.625, 0.75 and 1 bucket on the same three. Nothing bounces, because even the Artemis at 50 damage clears 1 percent of the Hades' 400 shield.
Neither side can clear the other in six rounds, so it ends in a draw:
Run the same battle through the per unit engine instead and you get the same winner, the same round count, and losses within a few percent:
| Losses | Large battle algorithm | Per unit engine |
|---|---|---|
| Prometheus | 482,999 | 496,463 |
| Artemis | 1,999,866 | 1,999,864 |
| Hades | 833,153 | 830,175 |
| Athena | 907,080 | 905,558 |
| Poseidon | 972,322 | 971,341 |
How close is it
Four battles, each run on both engines in dev with no tech on either side:
| Battle | Units | Winner | Rounds | Attacker losses, per unit | Attacker losses, large battle |
|---|---|---|---|---|---|
| 1M Prometheus + 2M Artemis vs 1M Hades + 1M Athena + 1M Poseidon | 6,000,000 | Draw | 6 | 2,496,792 | 2,482,717 |
| 1M Prometheus + 2M Artemis vs 500k Artemis + 500k Atlas + 500k Missile Battery | 4,500,000 | Attacker | 3 | 40,858 | 39,806 |
| 1.5M Athena + 1.5M Apollo vs 800k Hades + 1.2M Laser Cannon + 400k Gauss Cannon | 5,400,000 | Defender | 4 | 3,000,000 | 3,000,000 |
| 2M Hades vs 1M Athena + 1.5M Artemis + 300k Plasma Cannon + 50k Large Decoy | 4,850,000 | Draw | 6 | 910,561 | 899,658 |
Same winner and same round count every time, with losses inside about 3 percent. Attacker losses come out a little lower on the large battle algorithm in all four. That gap is not run to run randomness: repeat the same battle and each engine lands on its own number within a fraction of a percent of itself. So treat the big engine as a close approximation of the small one rather than an identical copy of it.
There is at least one known shortcut behind that. The per unit engine ends a rapid fire chain when a shot is absorbed by a shield. The large battle algorithm works out its shot count from the rapid fire ratings alone, so in a battle full of shots that bounce it hands out a few more shots than the per unit engine would.
There is no upper limit
Because the work does not scale with the number of units, there is no fleet size the large battle algorithm cannot resolve. Here is 130,000,000 attacking units against 80,000,000 defending units, which came back as fast as any other simulation. The defender's 10,000,000 Plasma Cannons sit below the fold of that panel, under the ship list:
This is what makes the biggest NPC encounters possible at all. Their fleets are far past anything the per unit engine could hold in memory.
Things that are in the code but never fire
The engine was ported over from the old game and carries a few branches that nothing in SFC2 uses:
- Mines. There is a whole path for mines that blow up when a fleet gets close, splitting their damage 75 percent onto the enemy and 25 percent onto their own side. No unit in SFC2 is a mine.
- Kamikaze units. A unit that destroys itself to land one hit. Nothing in SFC2 is one.
- Piercing. Shots that skip the shield entirely. No unit pierces.
- Multifire. More than one base shot per unit per round. Every unit in SFC2 fires once.
If you have read the old SFC1 version of this page, the details there do not describe what SFC2 runs. That version used 20 buckets of remaining hull rather than 101 buckets of damage taken, spread its shots target by target with a separate roll for each, and switched between a normal and a Poisson distribution depending on how many hits a unit was averaging. SFC2 uses one Poisson distribution throughout. It builds the exact Poisson table out to 200 hits, and falls back to a normal approximation past that, or straight away if a unit is averaging more than 400 hits in a round.
Comments
No comments yet.
Sign in to leave a comment.