A random lance corpus #
sds lances draws lances the way MegaMek's own force generator draws them, and
writes each one in the shape sds_core::formation reads. It exists so that a
question of the form "what does the bot actually meet?" is answered against
forces somebody could field rather than against forces nobody would.
./sds.sh lances --count 20000 --out runs/lances.jsonl
cargo run --release -p sds-core --example lances -- runs/lances.jsonl
Why not uniform sampling #
Four Meks picked at random from the four thousand in the database is not a force. It has no faction, no year and no doctrine behind it, and the compositions that matter are exactly the ones it cannot produce: a corpus built that way had no Anvil, no Order and no Rifle lance in twenty thousand forces, which made it unable to answer the question it was built for. A corpus that cannot produce the cases anyone cares about is worse than no corpus, because its numbers look real.
megamek.client.ratgenerator is the availability data MegaMek draws its own
forces from - who fields what, in which year, at which equipment rating. A
lance out of it is a lance a player could meet.
What the corpus is #
JSON Lines, in SdsForces's shape, so a lance corpus and a suite dump are read
by the same consumer:
- The first line is a manifest: the seed, the count, the sampling settings, and what MegaMek answered with - era count, faction count, model count and the size of the design database. Two corpora whose manifests differ are not comparable, and the manifest is what says so.
- Every line after it is one lance:
scenariois the draw that produced it (faction/year, plus/ratingwhen one was drawn),forceis its ordinal, andunitsis the four machines.
It is not committed, and will not be #
The bytes carry every machine's tonnage, armour, movement and weapons. That is
a bulk copy of MegaMek's CC BY-NC-SA unit data, and this repository ships none
of it - the same reason crates/sds-core/tests/corpus/forces.jsonl is
regenerated rather than shipped.
Membership alone would be small - four names and a faction is eighty bytes, so twenty thousand lances is under two megabytes - but it would not be useful. Nothing can classify a list of names without the facts, so a committed membership file would have to be joined against a dump on every use, and it would still be a bulk transcription of MegaMek's availability tables. What is committed instead is the generator, its defaults, and the manifest format that says which corpus a number came from.
Reproducibility #
Two seeded streams, and neither reads a clock: one picks the faction, year and
rating, the other picks machines out of the table those build. Raising
--count extends a corpus rather than reshuffling it.
The draw over the table is ours rather than UnitTable.generateUnits, and
the two reasons are worth knowing before changing it back:
generateUnitdraws throughCompute.randomInt, which is process-wide. Reading a design reaches the same RNG -Observation.averageDamageresolves a cluster weapon by building a throwaway game and asking the handler - so one stream for both makes each lance depend on which weapons the lance before it carried.- A table does not hand its entries back in a fixed order. The generator fills its era data on background threads and a table inherits their insertion order, so the same seed mapped to different machines on about one run in three until the entries were sorted by their own text first.
What that costs is the salvage branch: salvagePct and the salvage table
are private, and a draw that read them would be back to calling
generateUnit. A drawn lance is what the faction fields, with nothing
recovered from somebody else's wreck in it.
How exact it is #
sds lances prints [sds] digest <sha256> over the lance lines, so "is this
the same corpus" is one comparison rather than a diff of eighty megabytes.
Two runs of the same seed agree on every parameter and every draw count,
and on about 99.6% of lances. The residual is one machine in one lance,
almost always another variant of the same chassis: a table entry can name a
chassis rather than a model, UnitTable.getMekSummary picks the variant when
it does, and it picks through the process-wide RNG that MegaMek's own
background loading also touches. Measured over five runs of 3,000: 0.07% to
0.43% of lances differed, one machine each.
That is two orders of magnitude below the resolution of any row in the table below, so it does not affect a rate. It does mean a digest is evidence of sameness and not of difference - quote the seed and the settings, and check a number you care about against a second seed rather than against a second run of the first.
A corpus is reproducible from (seed, count, size, scope, ratings) against a
given MegaMek release. A different release has different availability data and
gives a different corpus; the manifest's era, faction, model and design counts
are what catch that.
Sampling settings #
--scope decides who may field a lance.
active(the default) draws a year first and then a faction thatFactionRecord.isActiveInYearsays existed in it. MegaMek carries 355 faction records across 39 eras and most of those pairs are a state that did not exist in that year. Drawing them anyway and letting the table come back empty is not neutral - it silently reweights the corpus towards whoever happened to be long-lived.anyignores the year. It is here to measure whatactiveis worth, not to be used.majorisactivewithout the recordsFactionRecord.isMinorflags.
--ratings decides equipment quality. unspecified is MegaMek's own default
and merges every rating band into one table. drawn picks one of the faction's
rating levels per lance, which splits the corpus by how well equipped a force
is.
Reading the output #
examples/lances.rs prints two columns per formation and the gap between them
is the point. satisfies is how many lances meet the criteria at all - what
the corpus can reach. wins is how many are classified as it once
formation::ordered's precedence has run. A formation rare because the corpus
cannot produce it and one rare because precedence eats it are different
findings with different fixes, and one column cannot tell them apart.
How big a row has to be before it means anything #
Two corpora of 20,000 at different seeds, same settings, compared row by row:
| row size (satisfies) | largest disagreement |
|---|---|
| above 2,000 | 1.7% |
| 700 to 2,000 | 16.4% |
| 200 to 700 | 8.7% |
| under 200 | 28.3% |
A binomial underestimates this and should not be used to plan a sample. A
row of 1,250 would be ±2.8% if every lance were an independent trial, and
Hammer moved 1,246 to 1,449. Lances are clustered: a draw fixes a faction and
a year and then takes four correlated designs out of one table, so the
effective sample is a good deal smaller than the lance count. Measure the
spread with a second seed rather than computing it.
What that leaves usable at 20,000 lances: rows above 2,000 carry a rate, rows between 200 and 2,000 carry an order of magnitude, and rows under 200 say "this happens" and nothing more.
The wins column is worse than satisfies at the same size, because it is
what survives precedence rather than what qualifies. Hammer moved 3 to 12
between the two seeds and Light Battle 143 to 109. Quote a wins figure only
where the satisfies row above it is large.
What this corpus cannot reach #
Order is the one formation of the thirty-three that no lance satisfied in
80,000 draws across four settings. It wants four machines of the same
model, and the generator draws each slot independently from the faction's
table, so more samples is not the fix. (Its faction: DC gate is not what
excludes it: Formation::qualifies does not read faction at all, because a
wire unit does not carry one.)
MegaMek.client.ratgenerator.FormationType.generateFormation draws a force
that satisfies a named formation, which is the tool for that row and for
Artillery Fire, Light Recon and Horde beside it. It is not what this
corpus is - a targeted draw answers "what does an Order lance look like" and
this answers "what turns up" - and the two should not be mixed in one file.
What twenty thousand lances came out as #
--seed 20260904 --count 20000, defaults otherwise: the corpus
crates/sds-core/tests/corpus/lances.txt records, which is what
formation::ORDER is pinned to. Every lance is classified as something,
because Support admits any four ground Meks and is last in the order.
The rows above 2,000 lances:
| formation | satisfies | wins | keeps |
|---|---|---|---|
| Support | 20,000 | 566 | 2.8% |
| Command | 12,504 | 973 | 7.8% |
| Security | 7,124 | 1,407 | 19.8% |
| Ranger | 6,967 | 1,624 | 23.3% |
| Urban | 6,426 | 1,703 | 26.5% |
| Heavy Battle | 5,737 | 1,587 | 27.7% |
| Anvil * | 5,285 | 0 | withheld |
| Battle * | 5,149 | 0 | withheld |
| Berserker/Close | 5,149 | 1,176 | 22.8% |
| Medium Battle | 2,927 | 1,391 | 47.5% |
| Fire Support | 2,418 | 1,231 | 50.9% |
* is a formation the catalogue withholds: it can qualify and can never be
handed to a force, so its wins is zero by construction rather than by
precedence.
What the order is derived from #
examples/precedence.rs reads a corpus and derives the precedence order from
it: each position goes to the formation with the fewest lances no formation
above it has already claimed. That is what the corpus is for, and the file it
writes is the artifact formation::ORDER is pinned to - the order is edited to
match by hand, and nothing recomputes it at build time.
Two things measured before that order was landed are worth keeping, because both are the sort of thing somebody will propose again:
Move the least-used formations to the top and repeat. It converges - a fixed point by iteration seven for one to four moved per round, a period-two cycle for five to ten, and a period-33 cycle at thirteen - and it is worse than one pass of the rule above on every measure, at every j. It is also unstable: the order it settles on moved a formation by up to 19 positions between corpus seeds, where the one-pass rule moves nothing by more than two.
Sort by the raw satisfies count. Simpler, and measurably worse: it leaves
Ranger 3.6% of lances where taking the residue leaves it 8.1%. What a
formation can still win depends on what everything above it took, and two
formations with the same count can overlap the order above them completely
differently.
The two sampling settings are worth less than they look. --ratings drawn
moves nothing above its own noise. --scope any gives almost the same
distribution and wastes 63% of its draws on faction-year pairs that did not
exist - 54,654 draws for 20,000 lances, against 21,436 for the default.