# Combat mechanics — stacks, flanking, capture

Corrections and additions to the mechanics used elsewhere in these files.
Several items here overturn earlier statements; those are marked.

## Reference implementation

`H:\Coding\eye-of-sauron` — a stable, permanently available checkout (it backs a
running Docker service). `src/eye_of_sauron/sim/engine.py` is a validated Python
port of the GC battle algorithm (itself a port of `Sim.Core/Battle.cs`),
reconciled against real battle logs. Its **mechanics** are exact where my own
JavaScript reconstruction was ~88% — prefer it for anything mechanical.

| File | Contains |
|---|---|
| `sim/engine.py` | battle loop, flanking, capture hook, victor predicates |
| `sim/details.py` | posture modifiers; `spam_penalty` (see caveat below) |
| `sim/models.py` | `Ship` fields incl. `is_flanker`, `capture`, `dfire` |
| `sim/comp_rules.py` | lead eligibility, uncappable rules, PR caps |
| `sim/counter.py` | `_FEZ_SHIPS`, counter search |
| `sim/patterns.py` | `SimConfig` tuning constants |
| `shiplist.csv` | raw ship export |

Running it against **live GC data** (rather than its own snapshot) —
`scratchpad/bridge.py` builds `Ship` objects from `gcc.ship_type` and calls
`simulate()`:

```bash
cd /h/Coding/eye-of-sauron && ./.venv/Scripts/python.exe /path/to/bridge.py
```

Python 3.13 lives at `.venv/Scripts/python.exe`; `python3` is not on PATH in
Git Bash.

### Two places it is not a GC oracle

1. **`spam_penalty()` is not a GC mechanic at all.** It is the author's hook for
   *theorising possible future changes* to GC. It returns `Decimal("1")` — a
   no-op — and nothing in the live game corresponds to it. Do not cite it as
   evidence about GC, in either direction.

2. **`_game_victor` is wiki-derived and does not match the live code.** Its
   docstring says "per the wiki": winner = whoever lost less PR, with a wipe
   exception. The live `f_com_attack3.cfm` predicate is stricter. Tested against
   **5,199 real battles using logged casualties** (so no simulation error — this
   isolates the predicate itself):

   | Predicate | Accuracy | FP | FN |
   |---|---|---|---|
   | `f_com_attack3.cfm` — defender loses ≥30% **and** `a_lost ≤ d_lost × 1.15` | **99.8%** | 6 | 3 |
   | `eye-of-sauron._game_victor` — `a_lost < d_lost` + wipe rule | 94.9% | 102 | 164 |

   The two agree on 95.1% of battles. **Use the CFML predicate for outcome
   work**; the engine's own `total_game_wins` will over-count attacker wins by
   roughly five points. (Worth reporting upstream.)

---

## 1. Stack slots are the resource

A side fields at most **10 stacks**, ordered by **total stack power,
descending**. A wave iterates `max(attackerStacks, defenderStacks)` slots.

At slot *i* there are three cases:

| Condition | What happens |
|---|---|
| Both sides have a live stack at *i* | **Paired exchange** — higher range fires first, then the other fires back |
| Both have a stack at *i* but one is **dead** | The survivor fires a **one-way flank** at a *random live* enemy stack |
| Only one side still has a stack at *i* (excess) | **`_excess_exchange`** — the excess stack and a *random live* enemy stack run a **full two-way** exchange |

**Correction.** An earlier version of `Ship-Balance-Analysis.md` said "lower
range fires first". It is the opposite: `if (d_range >= a_range) defender
first`, so **higher range fires first**, ties to the defender. In
`_excess_exchange` a tie goes to the flanker.

**Correction.** I previously read "flanking" as simply "took no return fire".
That is the battle-log *wording*, not the mechanic. In the engine a **flank is
what an unpaired or dead-slot stack does** — a shot at a randomly chosen live
enemy. It is gated by slot arithmetic, not by any ship flag; `is_flanker` is a
composition-search hint, not a combat property.

### Why this makes stack count valuable

Every stack you field is an attack every wave. Fielding more stacks than the
enemy converts the surplus directly into extra exchanges against random enemy
stacks. Conversely, a stack of yours that dies early hands the enemy a free
one-way flank at the slot it used to occupy.

`_excess_exchange` being **two-way** matters: an enemy stack that still holds
an unspent attack shoots your excess stack back and can wipe it. Modelling it
as one-way under-kills leftovers — a documented bug the reference fixed after a
real battle disagreed (576 P.Zeus survived in sim, 0 in reality).

---

## 2. Dummies

A **dummy** is a padding stack — one ship up to about one turn's production —
fielded to occupy a slot. Because stacks sort on total power, dummies fall to
the bottom of the order automatically.

They do three things:

1. **Add slots**, converting into flank shots when you out-stack the enemy.
2. **Hold a slot alive**, denying the enemy the free flank they would get if
   that slot were empty or dead.
3. **Absorb** random flank shots that would otherwise land on a real stack.

The reference treats a stack as a dummy at **≤3 ships**, or **<1% of side PR**
(`DUMMY_MAX_SHIPS`, `DUMMY_MAX_PR_FRACTION`).

**The important exception:** dummies are *not* harmless when Strafez Queen or
King is on the field. The engine's own comment is blunt — *"a 1m PR Broadsword
can kill a 100m PR Queen"*. That single fact drives the whole fez/anti-fez
positioning game.

---

## 3. Fez — the Strafez line

Class **0 = Special** (the class label is only appended to the display name
when `class > 0`, which is why battle logs read "Strafez Queen", not "Strafez
Queen Special"). These were excluded from all my earlier analysis by a
`class >= 1` filter — a significant omission, since they are central to the meta.

| Ship | PR | BR | Hull | Range | LR | RF | Weapon | E / K / M / C shields |
|---|---|---|---|---|---|---|---|---|
| Small Strafez Fodder | 55 | 800 | 200 | 1 | 0 | 0 | — | −0.10 / −0.75 / 0.50 / 0.50 |
| Large Strafez Fodder | 205 | 180 | 800 | 1 | 0 | 0 | — | −0.10 / −0.75 / 0.50 / 0.50 |
| Small Strafez Runner | 115 | 400 | **1** | 1 | 1 | 1 | 115 C | 0 / **−0.95** / **0.9995** / 0.25 |
| Large Strafez Runner | 435 | 100 | **2** | 5 | 1 | 0 | 400 C | 0 / **−0.95** / 0.25 / **0.9995** |
| Strafez Queen | 896 | 40 | 25 | 7 | 1 | 0 | 700 C | 0 / **−0.95** / **0.9995** / **0.9995** |
| Strafez King | 890 | 50 | 200 | 8 | 1 | 0 | 550 C | −0.15 / −0.15 / 0.90 / 0.90 |

`_FEZ_SHIPS` in the reference is the four armed hulls (both Runners, Queen,
King) — the two Fodder hulls are pure padding.

### How fez are countered

The Queen and both Runners take **0.05% from missile and chemical** and **195%
from kinetic**. Missile and chem weapons literally cannot kill them; kinetic
deletes them. Strafez King is the outlier — 115% from both energy and kinetic,
10% from missile/chem — so it is far less matchup-locked.

Because their hulls are 1–25, a single cheap kinetic stack placed to meet them
wipes the stack outright. Damage into a Strafez Queen per 1k PR:

| Ship | Race | PR | Kinetic | Damage into Queen /1k PR |
|---|---|---|---|---|
| R.Pinnace | Collective | 162 | 200 | 2,407 |
| Ruby | Guardian | 11,268 | 13,000 | 2,250 |
| P.Thor | Terran | 1,624 | 1,600 | 1,921 |
| Tourmaline | Guardian | 6,510 | 6,000 | 1,798 |
| F.Broadsword | Neutral | 1,116 | 781 | 1,365 |
| D.Pike | Marauder | 1,276 | 500 | 1,156 |

This is the "fez killer placed down low" play: you size a cheap kinetic stack
so it lands in the low slots where the enemy's fez sit.

### Positioning

Slot position is set purely by **total stack power**, so unit count is the only
control. That cuts both ways, and the reference guards against it explicitly —
*"a 75%-of-fleet-PR fez/flanker becomes the de facto lead no matter which slot
the picker intended"*. `SimConfig.non_lead_max_pr_fraction = 0.20` caps a
non-lead stack at 20% of fleet PR for exactly this reason.

`is_flanker` marks ships that may never be the slot-0 lead — P.Apollo,
G.Rhyolite, Tourmaline, R.Pinnace among them. Tourmaline is the clearest case:
1,301 damage per 1k PR, the highest in the game, on a 6,510 PR hull with
longrange — devastating in a support slot, and killable by the same cheap
kinetic answer that handles a fez.

---

## 4. Capture

**Missed entirely in earlier analysis.** `ship_type.capture` is live and large.

**Whether a killed ship can be taken is decided by three independent gates. All
three must pass:**

1. **The ship's race must be Terran, Marauder or A.Miner** (races 1 / 2 / 6).
   Checked on the *ship's* race, not the owner's. Neutral, Guardian, Collective
   and Viral hulls are never capturable.
2. **The ship's `capture` must not be −100.** −100 is the invulnerability
   marker. This is what protects Miner `G.*` and Marauder `D.*`.
3. **The ship must not be a Starbase or Scout** (class 8 / 21).

Separately, the *capturing* ship needs `capture > 0`. The number is the
**percentage of the killed stack that it takes**: `fix(killed × capture/100)`.
V.Borrelly at 75 that kills 100 eligible ships captures **75 of them**. A
Collective `H.*` hull at 100 takes **everything** eligible it kills.

| Ships | Capture | Effect |
|---|---|---|
| Collective `H.*` (Galleon, Barkentine, Brigantine, Man-O-war, Corsair, Suzerain, Overlord) | **100** | takes everything eligible it kills |
| Viral `V.*` (Borrelly, Triton) | **75** | |
| Collective `R.*` (Schooner, Snow, Monitor) | **50** | |
| Marauder `D.*` (Ray, Hammerhead, Pike, Sting) | **−100** | invulnerable |
| Miner `G.*` (Livid, Livid (r), Lictor, Sapphire, Rhyolite) | **−100** | invulnerable |
| everything else | 0 | takes nothing; capturable if gates 1 and 3 pass |

Every negative value in the player roster is exactly −100; there is no partial
immunity.

### The prefixes are not one system

It is tempting to read `P.` / `D.` / `G.` as a single "protected" convention.
They are two overlapping systems with **different** membership:

| Prefix | Capture-immune? | Viral-reversible? |
|---|---|---|
| `D.` (Marauder) | **Yes** (−100) | No |
| `G.` (Miner) | **Yes** (−100) | No |
| `P.` (Terran) | **No — capturable** | No |

So `P.` hulls can be taken by a Collective `H.*` fleet but cannot be reverse
engineered by Viral, while `D.` and `G.` are protected from both. A ship renamed
across either boundary silently changes its behaviour in that system — which
strengthens the case in `../Ship-Balance-Changes.md` for explicit `ship_type`
flags rather than name matching.

(The reference's `_UNCAPPABLE_PREFIXES = ("G.", "D.")` and
`_UNCAPPABLE_RACES = {Neutral, Guardian, Viral}` are a shorthand for gates 1 and
2 combined; the underlying rule is the three gates above.)

Capture is a real strategic axis I had no visibility into: a Collective `H.*`
fleet converts eligible kills into its own ships, and "prefer uncappable" is a
genuine counter-selection toggle in the reference. It also means Terran,
Marauder and Miner players pay a hidden tax when trading with Collective or
Viral that Guardian and Neutral fleets do not.

### Measured capture volume

Attacker at 130% PR against a capturable Tiger fleet, engine-measured:

| Attacker | Capture stat | Defender PR destroyed | **PR captured** |
|---|---|---|---|
| H.Suzerain | 100 | 100.0M | **100.0M** |
| H.Galleon | 100 | 67.6M | **67.6M** |
| V.Triton | 75 | 63.3M | 47.5M |
| H.Overlord | 100 | 22.0M | 22.0M |
| R.Monitor | 50 | 24.4M | 12.2M |
| V.Borrelly | 75 | 8.0M | 6.0M |
| R.Sloop / Angel / D.Pike | 0 / 0 / −100 | — | **0** |

A Collective `H.*` fleet that wins does not merely destroy the enemy — **it
takes the entire eligible fleet**. Net swing is roughly double an ordinary
race's for the same battle.

**This is the single biggest reason raw combat metrics understate Collective.**
Ranked on damage per PR it looks like the weakest race; ranked on net PR swing
per turn with capture priced in, it sits near the top — which is where field
experience puts it. Note also that `eye-of-sauron`'s campaign model
deliberately does **not** compound captures (stolen ships never re-field), so
even the corrected numbers are a floor, not a ceiling.

---

## 4a. Starbases: defence-only, and the damage-protection privilege

Two rules that together define the class, both easy to miss.

**Starbases and Scouts can never attack.** `f_com_attack3.cfm` builds the
attacker's fleet from RR2, which carries
`AND type NOT IN (SELECT id FROM ship_type WHERE class IN (8,21))`. RR1 (the
defender) has **no** such filter. So class 8 and 21 defend and are never sent on
an attack. Corroborated in the battle record: M.Lakko shows 34,296 defensive
units and **zero** offensive units.

**Starbases are the only hulls you can build without dropping damage
protection.** From `Modules/Ships/Builder.cfm:250-297`:

```cfml
<cfif class IS NOT 8 AND class IS NOT 21>
    <cfset Query2 = "Go Daddy">
</cfif>
...
<cfif Query2 IS NOT FALSE>, protection = CURRENT_TIMESTAMP(), planetlost = 0</cfif>
```

DP is gated on **`planetlost >= 3`** (`s_com_attack_req.cfm:207`). Building any
class other than 8 or 21 sets **`planetlost = 0`**, ending your protection
immediately. Building a starbase leaves it intact.

DP windows are 24–48 hours by server (`APPLICATION.server<N>_dp` = 48 / 24 / 48 /
36 / 24). During one you are invulnerable, and **starbases are the only way to
convert turns into fleet without giving that up**. Everyone else chooses between
staying protected and building nothing, or building and becoming attackable.

**Consequence for evaluation:** starbases must be judged on PR banked per turn
during a protected window, not on combat efficiency. Ranked that way:

| Starbase | Race | PR | BR | ppt | PR per 90-turn bank |
|---|---|---|---|---|---|
| G.Diamond | Guardian | 101,858 | 1 | **101,858** | 9.2M |
| A.Kryo | A.Miner | 87,781 | 1 | 87,781 | 7.9M |
| Scorpion | Terran | 43,681 | 2 | 87,362 | 7.9M |
| M.Lakko | A.Miner | 24,444 | 3 | 73,332 | 6.6M |
| B.Chiron | Viral | 18,482 | 3 | 55,446 | 5.0M |
| Chimaera | Terran | 21,393 | 2 | 42,786 | 3.9M |
| H.Man-O-war | Collective | 450 | 90 | 40,500 | 3.6M |
| A.Hoko | A.Miner | 9,945 | 3 | 29,835 | 2.7M |

Ranking the same hulls by *defensive trade* inverts this almost exactly —
Chimaera first, Scorpion last. That ranking cost me a wrong recommendation
(see [../Ship-Balance-Changes.md](../Ship-Balance-Changes.md) §2) and is the
sharpest example in these files of a reasonable-looking metric measuring the
wrong thing.

---

## 5. Posture and fire budgets

| Posture | Attacker | Defender |
|---|---|---|
| Careful | 0.50 | 0.50 |
| Normal | 0.95 | 1.00 |
| Aggressive | 1.75 | 1.99 |

`dfire = 2` and `afire = 1` for every **player** ship. The nine `K.*` hulls at
`dfire = 4` are **Kal-Zul (race 7)** plus one Berserker (race 9) — NPC, outside
PvP scope.

**There is no anti-spam mechanic in GC.** Nothing penalises concentrating PR
into one lead stack. (`spam_penalty` in the reference is a hook for theorising
future changes, not a live rule — see the caveat at the top of this file.)

---

## 6. Stack regimes

Simulated at equal PR with my own engine (which uses the correct CFML win
predicate but approximates flank redirection), so **directional rather than
exact** — worth re-running through `eye-of-sauron` before relying on the
numbers.

Attacker win %, rows = attacker regime:

| | full (10 real) | half (5 + 3 dummies) | spam (2 + 8 dummies) | solo (1) |
|---|---|---|---|---|
| **full** | 48.6 | 49.8 | 51.4 | 85.9 |
| **half** | **61.2** | 50.3 | **13.7** | 78.2 |
| **spam** | 49.0 | **78.4** | 48.0 | 52.6 |
| **solo** | 14.8 | 23.6 | 46.2 | 43.3 |

There is a genuine rock-paper-scissors: **half beats full, spam beats half,
full is roughly even with spam.** Solo loses to everything. Spam's edge over
half is the largest single effect measured (78.4 vs 13.7) — 10 slots of which
8 cost almost nothing, against 8 slots with 20% of PR sunk into padding.

Because `spam_penalty` is a no-op, nothing in the engine pushes back on the
spam shape. Whether that is intended is a design question worth asking.

---

## 7. What this invalidates

| Earlier claim | Status |
|---|---|
| "Lower range fires first" | **Wrong** — higher fires first, ties to defender |
| "Flanking = taking no return fire" | **Wrong** — a flank is an unpaired/dead-slot stack's shot at a random target |
| `longrange` correlates ~0, so it is worthless | **Incomplete** — longrange hulls have systematically lower hull/PR (1.020 vs 1.916). The flag is paid for; the correlation cancels because it is a trade |
| Analysis covered "every buildable player combat ship" | **Wrong** — a `class >= 1` filter silently excluded all six Special/Strafez hulls |
| Capture not mentioned | **Missing mechanic**, and a large one |
| Cross-race hulls in battle logs are unexplained | Partly explained — capture moves Terran / Marauder / A.Miner hulls to Collective and Viral fleets. Does not yet explain Collective hulls in Miner fleets |
| Stack counts fixed at 6 in all simulations | **Unrepresentative** — stack count is itself a primary lever |
