Cut 9: the die is not a die
9.1, blocking. FireControl seeded CultMath's xorshift32 per shot and drew once. NextFloat returns the top 24 bits of the FIRST round, and one round cannot carry ShotId's low-bit variation into the high bits -- so every zone's hit roll spanned about an eighth, with the threshold fixed at zone creation. A shot was a step function of p: below the band it never hit, above it always did. An unaided shooter hit 0 of 18,926 shots. That is the operator's "no hits registering," and being stable per zone is why it came and went. The seed derivation is unchanged -- still a pure function of (zone, shot id), Cut 6b's rule -- and only its diffusion into the first output changes, via an fmix32 finalizer. Measured across six zone seeds: hit fraction at p=.3 went from 0.0% in five of six zones to 29-31%, and at p=.6 to 58-61%, with the roll spanning the full range. The defect survived three cuts because nothing in the campaign measured a distribution -- every fixture uses accuracy 1 or 0, and a constant satisfies a determinism test perfectly. TheDieIsUniform now asserts the observed hit fraction is within .08 of p across several zone seeds, which is the test whose absence was the real defect. 9.2. Sigma is floored at half a cell. HullKernel approximates a continuous Gaussian with a discrete sum over occupied cells, and that sum collapses once sigma falls well under one cell against an aim point that is not exactly on a cell centre -- true of every shipped hull. Past Precision ~4, pOnHull went to zero and a better targeting system made the weapon unable to hit anything. 9.3. An active entity's Target is now always a live entity it has info on. Cut 8's _active guard could never have fixed the operator's LockWeapon crash -- that code only runs on active entities, and cut8's own harness header says so. The live route was dock, target dies, undock; Deactivate disposed the subscription that nulls a stale Target and Activate reseeded EntityInfoGathered without reconciling it. Still not a TryGetValue in LockWeapon. 229 tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
16d8493e0c
commit
6db6dc78f3
4 changed files with 797 additions and 5 deletions
|
|
@ -344,6 +344,22 @@ public static class FireControl
|
|||
// so whether a gated shot draws is unobservable from anywhere. The claim this comment used to make is
|
||||
// gone with the stream it was about, and the test that asserted it became vacuous the same moment; what
|
||||
// replaced it pins the live rule instead (combat touches no shared stream, on any path).
|
||||
// 9.1 (docs/fire-control-cut.md): fmix32, MurmurHash3's 32-bit finalizer -- xor-shift, multiply,
|
||||
// xor-shift, multiply, xor-shift. Diffuses a structured seed (here, a zone seed XORed with a small,
|
||||
// low-bits-only ShotId) across every bit before the first xorshift draw reads it, so two seeds that
|
||||
// differ only in ShotId's low bits still land on uncorrelated first outputs. Caller-side mixing only
|
||||
// -- CultMath.Random itself is unchanged (see Commit's own note on why that follow-up is a separate
|
||||
// CultLib cut).
|
||||
private static uint MixSeed(uint seed)
|
||||
{
|
||||
seed ^= seed >> 16;
|
||||
seed *= 0x85ebca6bu;
|
||||
seed ^= seed >> 13;
|
||||
seed *= 0xc2b2ae35u;
|
||||
seed ^= seed >> 16;
|
||||
return seed;
|
||||
}
|
||||
|
||||
private static ShotOutcome Commit(Zone zone, PendingShot shot, float now)
|
||||
{
|
||||
// Cut 6b, 6.1 (Soul finding 6): a shot's dice belong to the shot, not to whatever else happened to
|
||||
|
|
@ -353,7 +369,16 @@ public static class FireControl
|
|||
// no matter what the UI drew from the shared stream in between. This generator is local and dies with
|
||||
// the call: nothing outside Commit may seed or advance a combat draw, and Commit may not read or write
|
||||
// a shared stream.
|
||||
var random = new Random((zone.CombatSeed * 2654435761u) ^ (uint) shot.ShotId | 1u);
|
||||
// 9.1 (docs/fire-control-cut.md): the per-shot seed is unchanged -- a pure function of
|
||||
// (zone identity, shot id), Cut 6b's own rule -- but CultMath.Random draws only one xorshift
|
||||
// round for NextFloat's first call, and one round does not diffuse a seed that differs from its
|
||||
// neighbours only in ShotId's low bits. Unmixed, every zone's first draw was a near-constant
|
||||
// function of ShotId (measured span ~0.125), making a shot a step function of p fixed at zone
|
||||
// creation, not a roll (Soul C1). MixSeed is a standard fmix32 finalizer (MurmurHash3): this is
|
||||
// a caller's obligation, not a CultMath defect -- seeding xorshift with a structured value and
|
||||
// drawing once is the documented way to get a correlated first output, and a follow-up to give
|
||||
// CultMath.Random a mixed-seed entry point of its own is recorded for a separate CultLib cut.
|
||||
var random = new Random(MixSeed((zone.CombatSeed * 2654435761u) ^ (uint) shot.ShotId | 1u));
|
||||
var p = shot.PBase;
|
||||
if (p > 0f && shot.Target != null)
|
||||
{
|
||||
|
|
@ -486,7 +511,16 @@ public static class FireControl
|
|||
// Higher Precision (a tighter group) gives a smaller sigma; the 1e-3 floor only guards a stray zero or
|
||||
// negative authored value; no authored Precision should ever reach it (GameplaySettings.UnaidedPrecision
|
||||
// is the deliberately-bad floor Precision(Entity) itself falls back to, same shape as UnaidedAccuracy).
|
||||
private static float Sigma(float precision) => 1f / max(precision, 1e-3f);
|
||||
//
|
||||
// 9.2 (docs/fire-control-cut.md, Soul C2): SigmaFloor guards a second, unrelated failure the 1e-3
|
||||
// floor above does nothing about. HullKernel approximates a continuous 2D Gaussian with a discrete
|
||||
// sum over occupied cells; that sum badly undershoots the continuous integral once sigma drops much
|
||||
// below one cell against an aim point that is not exactly on a cell centre -- true of every shipped
|
||||
// hull, none of whose centres of mass are integral. Past that point pOnHull collapsed toward zero as
|
||||
// Precision kept climbing, so a better targeting system made a shot un-fireable. Clamping sigma at
|
||||
// half a cell keeps the discrete sum a faithful share of the kernel over the whole Precision domain.
|
||||
private const float SigmaFloor = 0.5f;
|
||||
private static float Sigma(float precision) => max(1f / max(precision, 1e-3f), SigmaFloor);
|
||||
|
||||
// w(cell) = exp(-d^2 / 2*sigma^2) over every occupied hull cell, `d` the planar cell-space distance from
|
||||
// the aim point, plus pOnHull = (sum of those weights) / (2*pi*sigma^2) -- the share of the full continuous
|
||||
|
|
@ -529,9 +563,14 @@ public static class FireControl
|
|||
|
||||
// A single weighted draw over the kernel's own occupied-cell weights -- the only way Commit picks a cell
|
||||
// once a shot has already passed the roll (R3: one roll decides damage). totalWeight is passed in rather
|
||||
// than resummed so this reads the identical mass HullKernel just computed. The trailing return guards only
|
||||
// floating-point rounding at the very end of the accumulation; it can never fall through for a shot that
|
||||
// reaches here, because the roll already established this hull has nonzero on-hull mass.
|
||||
// than resummed so this reads the identical mass HullKernel just computed.
|
||||
//
|
||||
// 9.2 (docs/fire-control-cut.md, Soul C2): the trailing return used to carry a claim that it "can
|
||||
// never fall through," which was false on its own terms -- it falls through whenever totalWeight == 0,
|
||||
// returning a fixed corner cell, and was unreachable only as a side effect of pOnHull collapsing to
|
||||
// zero and gating PBase to zero first. Now that Sigma floors sigma so pOnHull no longer collapses,
|
||||
// that accidental gate is gone too; the return stays as an explicit floating-point-rounding guard, not
|
||||
// a documented invariant.
|
||||
private static int2 WeightedPick(int2[] cells, float[] weights, float totalWeight, Random random)
|
||||
{
|
||||
var roll = random.NextFloat() * totalWeight;
|
||||
|
|
|
|||
Loading…
Reference in a new issue