Skip to content
qbisiPublic

About

Mechabellum battle record tooling and simulation

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Document conventions

Two kinds of document live here, beside the terminology that names what they talk about, and which kind a document is decides what it may contain and who has to change when reality disagrees with it.

Directory Answerable to When it disagrees with reality
rules/ the build the document is wrong, and re-measuring is the fix
spec/ its readers the code is wrong, and changing the code is the fix

A rules document describes a mechanism the game decides. It is pinned to a build, it carries the evidence for what it claims, and it goes stale when the game changes. Most are the prose beside a machine-readable table in config/: the table is what code reads, the document says what the entries mean.

A spec defines a shape something must conform to. spec/ is filed by the crate that owns each contract, so a document and the code that must satisfy it are found the same way.

The language rule below governs both. After it, the sections up to "A spec's shared spine" state what a rules document may claim and on what evidence, and the rest of the file states the convention for a spec.

Language

Every document is written in English, and none is translated: a second copy is a second thing to keep in step, and it falls behind. Chinese is written in terminology/ alone, which maps each English term to the one Chinese word a discussion uses for it.

A document names a unit, a technology or anything else the game names by its official English name, as terminology/ lists it, and never by its Chinese one.

A rule is something a reader can rely on

A rules document states rules a reader can act on without going back to check. That is the whole standard, and it decides what may appear there.

A rule is a claim about the build, carried with the scope it holds in. The scope is not a hedge, it is what makes the rule usable: a reader who knows where a rule stops can work inside it and knows to stop at the edge. So state what a rule does not cover beside the rule itself, because a claim just outside a closed scope is unverified however obvious an extension of it looks.

Three things read like rules and are not.

  • What our own code reproduces. That a simulator matches a corpus tick for tick is a fact about the simulator. The game is not answerable to it.
  • A single run's trace. Tick numbers lifted from one replay record an observation. The rule is whatever that observation demonstrated, and it is the rule that belongs here.
  • Confidence labels and review status. strongly_supported and its neighbours say how well a question was answered, not what the game does.

None of them belongs in a tracked document; the commit that settled a rule is where its history goes. rules/combat.md is the worked example: every entry is a scoped claim, with the boundary it does not cover stated next to it.

Two gates

Reproducing a replay is not proving a mechanism. Byte-identical trajectories over several independent replays support the main path they traverse, strongly. They say nothing about branches nothing took, and on their own they cannot fix a single number. So a mechanism and a number are held to different standards before either is written as a rule.

A mechanism closes on the build's decompiled code: a branch, a state transition, a formula, a call chain. One that branches too widely to close exhaustively may be concluded on risk, but only with all four of: a structure that is self-consistent, several mutually independent native trajectories that match tick for tick inside a declared scope, no known counterexample and no unexplained first divergence inside that scope, and the unverified assumptions written down.

A number is any constant that reaches the code: a radius, a threshold, an interval, a priority, a time horizon. Every one traces to exactly one of the build's decompiled code, its extracted resources or serialized config, or a native field observed at run time through the Adapter. A number without one is a hypothesis: it does not enter the real code path and is not written as a fact about the game. Four things look like evidence for a number and are not: a trajectory that fits, an outcome that matches, an older project's config, and whatever value makes the implementation convenient. The number gate never relaxes because the mechanism around it was concluded on risk.

A question reopens on an event, not on a feeling: a counterexample, a new build, or a task that widens the declared scope. Re-researching a closed question because it feels incomplete is the most expensive habit available.

Evidence a rule may cite

A rule outlives the build it was read on only if its evidence can be read again on the next one. So a rules document cites evidence of three kinds, each of which a reader can reproduce from tracked files and a build:

  • A call chain in the build, named by class and method: FightingState.Update calls FightCoreSystem.TryDstroyTower. Names survive a new build; ISIL line numbers, addresses and static-field offsets do not, so they are not cited. scripts/decomp/decompile.py makes a build's dump and scripts/decomp/decomp-diff.py says which declarations changed between two.
  • A table of the build's data, named by its object and field: ConfigDataContainer.towerStrengthenDatas.life, or a level0 object such as MechSkillGroupData. The values live in config/, generated by a scripts/extract-* script from the build's export, and the document says what they mean. It does not copy them: a copied value is a second source that goes stale with the next build.
  • A recording, pinned in tests/<topic>/ with the layout and the record script that make it. The pin names the fight, and the fight's hashes are the measurement.

Four things are not evidence and stay out:

  • statistics over a corpus or a capture ("148,466 of 148,595 calls", "292 rounds"), which change with every recording added;
  • values copied from config/ or from a build's tables;
  • anything under work/, which is not tracked, and level0 path ids, which move between builds;
  • a single run's numbers standing in for the rule they illustrate.

A worked example that needs numbers says which units and levels it uses, so a reader can recompute it from config/. A number a single recording happened to show, such as the tick a shot left on, stays with the fixture: it moves silently when the pin is re-recorded, where the rule it illustrates does not.

A rules document names no game version and ends in ## Evidence, so that a reader can tell what holds on the version GAME_VERSION pins from what may have moved. It has up to four parts, in this order, and leaves out one it has nothing for:

  • ### Recorded: each claim a fight shows, citing the fight documents under tests/ that pin it. CI verifies them on every change, and they are re-recorded whenever the version moves, so these claims move with it.
  • ### Replayed: each claim the replay corpus of the pinned version shows, citing scripts/corpus/verify-matches.py, which replays every round of the match documents scripts/corpus/export-replay-corpus.py converts from that corpus, or scripts/corpus/match-replays.py, which fights every round of them with the game. The corpus workflow runs verify-matches.py on every master commit, outside the gate: a replay the corpus adds does not keep a change from merging, so CI does not hold these claims the way it holds the recorded ones. A claim the corpus of another version showed is not established here until this version's corpus shows it too.
  • ### Read: each claim read from the build, naming in backticks the members it rests on, Class.member, as the class declares them, a nested class as Outer.Inner.member. They are the document's anchors.
  • ### Not established: what the document does not claim, and why.

scripts/decomp/rules-anchors.py resolves every anchor in the pinned version's dump. When the version moves, scripts/decomp/rules-anchors.py --since <old version> lists the read claims whose anchors changed, a method's instructions or a member's declaration; each is read again on the new version before the move is merged. A claim whose anchors stand still carries over. scripts/check/check-docs.py holds the section's shape: a recorded claim that cites no pinned fight, or a read claim that names no anchor, fails.

A spec's shared spine

Two sections are required of every spec, whatever it specifies.

## Scope is the first section. It says what this contract defines, what it deliberately does not, and how it divides with its siblings. A reader who stops after Scope should already know whether this is the document they want.

## Unresolved is the last section. It holds design choices nobody has made yet, each one stated as the question it is. A spec with nothing open writes None. under the heading rather than dropping it, because an absent section cannot be told apart from a question nobody asked.

Unresolved is for decisions, never for work. "Whether a match document records how the match ended" is a decision. "The deployment executor does not exist" is work, and work belongs in the pull request that does it.

A third case is neither. Something observed disagrees with this spec, and nobody has yet decided whether the spec is wrong or the reading was. That is an issue, and CONTRIBUTING.md says what becomes one and how it leaves.

Four things are banned from every spec:

  • a Status section, or any statement of how much is implemented;
  • corpus counts, pass rates and measurements;
  • the evidence or provenance that produced a rule, including decompilation traces and file hashes;
  • an account of how the design was arrived at, or of what an earlier version got wrong.

None of that is worthless. The evidence belongs in rules/, in the form the section above allows; the rest in the pull request or the commit that made the change.

Three kinds of spec

The spine is shared. What sits between Scope and Unresolved depends on what is being specified.

Kind Documents Required sections
Document format layout, fight, state, match, action, layout replay, match replay, turn, mcfr, unit-rules the shape of the document, Normal form, Excluded fields
Interface contract adapter, cli, session each operation with its arguments, its result and what it refuses; an error taxonomy
Algorithm contract layout generation, architecture, rvo, quadtree the determinism invariants; the fidelity boundary

A required section may be delegated to the sibling that owns it, and the spec that delegates says where. What is not allowed is silence: a missing section with no owner named is a gap.

Excluded fields is the counterpart of Scope, and it is what stops a settled question from being reopened: it names what the format deliberately leaves out and why, so a reader who expected a field learns it was considered.

The shape may be one section called Document shape, as in layout.md, or a run of named sections that between them account for every field, as in match.md and state.md. What matters is that no field is undescribed, not which heading describes it. Prefer named sections once one Document shape would run long enough that a reader cannot find a field in it.

Normal form states the canonical order of every collection, so that two documents describing one position are the same document.

An error taxonomy is what makes an interface contract testable. A caller has to be able to distinguish a refusal it should retry from one it should not.

A fidelity boundary states what the reproduction is faithful to and where it stops. It is a scope statement, not a progress report, which is the distinction the next section is about.

Boundary is not progress

Both say "X is not covered here", and telling them apart is the rule that does the most work.

A boundary is a property of the contract. It stays true until the contract changes, and it belongs in Scope. Progress is a property of the code. It is stale the day it is written, and it belongs nowhere tracked: the code and the simulator's refusals say it.

The test is to rewrite the sentence in the present tense with no current, yet, still, or not implemented. If it survives, it is a boundary. If it collapses into nothing, it was progress.

Written as Reads as Belongs in
"the simulator does not yet load map objects" nothing survives the simulator's refusal
"a layout carries no map objects" a rule a reader can act on Scope
"the adapter cannot capture a full state today" nothing survives nowhere tracked
"a capture covers the layout projection" the contract's edge Scope

The same test catches a section title. Current adapter compiler and Loading and current kernel boundary both name a moment rather than a contract.

What a machine can check

scripts/check/check-docs.py enforces the mechanical half of this file. Run it from the repository root, and make it pass before a document change lands:

python3 scripts/check/check-docs.py

Among what it checks:

  • Every relative link resolves, the #anchor half included. A renamed section silently breaks every link into it, which is the failure most likely to happen without anyone noticing.
  • Every readme is spelled README.md.
  • Every spec under docs/spec/ is classified into one of the three kinds above. A new spec fails until it is classified, so none sits unchecked because nobody remembered it existed.
  • Every converted spec opens with Scope and closes with Unresolved, and carries no section titled Status or beginning Current.
  • No document says the same paragraph twice. A repeated paragraph is a paste gone wrong or a scripted replacement that matched more than it meant to, and the link check cannot see either: one such replacement once grew a 101-line readme to 55,000 lines. Table rows, short lines and fenced code may repeat.
  • No tracked file writes Chinese outside terminology/, but for the localization and the scripts that read it. A Chinese identifier, such as a replay named by its players, is quoted in backticks.

The checker also holds the list of specs that predate this convention, and that list is the only record of which ones are left. It fails in both directions: a spec still on the list that has started conforming is an error too, so converting one means deleting its line.

What no checker can do is tell whether a sentence is true. Nothing catches a rules document that quietly stopped describing the build, or a spec the code has drifted away from. Those need a reader, which is what the rest of this file is written for.

Worked examples

Every spec follows this convention, so any of them answers a question about form. For a document format copy match.md or state.md; for an interface contract adapter.md, whose error taxonomy is the fullest; for an algorithm contract rvo.md, whose fidelity boundary names what it does not cover rather than implying coverage.

The checker holds the list, not this page, and it fails when a new spec is neither classified nor conforming. So the claim in the paragraph above cannot quietly stop being true.

About

Mechabellum battle record tooling and simulation

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages