Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
/ * This Source Code Form is subject to the terms of the Mozilla Public
* License , v . 2.0 . If a copy of the MPL was not distributed with this
* file , You can obtain one at https : //mozilla.org/MPL/2.0/. */
using System ;
using System.Collections.Generic ;
using System.IO ;
using System.Linq ;
using CultMath ;
using GameCult.Caching ;
using UniRx ;
using Xunit ;
using static CultMath . math ;
using float2 = CultMath . float2 ;
using float3 = CultMath . float3 ;
using int2 = CultMath . int2 ;
// Cut 12.2 (docs/fire-control-cut.md, "Where a hit lands"): a hit's placement is priced and drawn from one
// silhouette, read at the bearing the target's facing implies AT COMMIT, not at fire. Builds its own
// fixtures, the convention every cut's test file follows. Fixtures follow 11.3: the shooter sits off the
// origin, fire times are nonzero, facings include ones where |fx| > |fy|, and weapon spread is nonzero
// wherever PSpread is asserted.
public sealed class FireControlCut12Tests : IDisposable
{
private readonly string _root = Path . Combine ( Path . GetTempPath ( ) , "aetheria-firecontrolcut12-" + Guid . NewGuid ( ) . ToString ( "N" ) ) ;
private string Catalog = > Path . Combine ( _root , "Aetheria.cc" ) ;
public FireControlCut12Tests ( ) = > Directory . CreateDirectory ( _root ) ;
private readonly List < CultCache > _openCaches = new List < CultCache > ( ) ;
public void Dispose ( )
{
foreach ( var c in _openCaches ) c . Dispose ( ) ;
Directory . Delete ( _root , true ) ;
}
private static GameplaySettings TestSettings ( ) = > new GameplaySettings
{
DefaultEntitySettings = new EntitySettings ( ) ,
Tiers = new [ ] { new RarityTier { Name = "Common" , Quality = . 5f , Rarity = 0 , Color = new float3 ( 1 , 1 , 1 ) } } ,
QualityPriceModifier = new ExponentialLerp ( ) ,
TargetDetectionInfoThreshold = . 1f ,
TargetArmorInfoThreshold = . 2f ,
TargetGearInfoThreshold = . 8f ,
FiringArc = 170 ,
CommitHorizon = . 5f ,
SchematicCellSize = 1f ,
UnaidedAccuracy = . 05f ,
UnaidedTracking = 10f ,
UnaidedPrecision = . 3f ,
AgentMinHitProbability = 0f ,
BeamResolveInterval = . 1f
} ;
private static PerformanceStat Constant ( float v ) = > new PerformanceStat { Min = v , Max = v } ;
private static Shape SolidShape ( int w , int h )
{
var shape = new Shape ( w , h ) ;
foreach ( var cell in shape . AllCoordinates ) shape [ cell ] = true ;
return shape ;
}
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
// S1 fix batch: a non-convex hull for the empty-lane sweep -- a solid square with its centre cell punched
// out, the same "holed7" shape Soul's own EdgeSweep probe used.
private static Shape HoledShape ( int w , int h )
{
var shape = SolidShape ( w , h ) ;
shape [ new int2 ( w / 2 , h / 2 ) ] = false ;
return shape ;
}
// A unit direction at `d` degrees, matching Soul's own probe convention (SoulProbe122b.Deg) -- used here to
// name an exact facing/bearing for a crash repro.
private static float2 Deg ( double d ) = > float2 ( ( float ) Math . Sin ( d * Math . PI / 180 ) , ( float ) Math . Cos ( d * Math . PI / 180 ) ) ;
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
private sealed class Engagement
{
public ItemManager Items ;
public Zone Zone ;
public Ship Shooter ;
public Ship Target ;
public EquippedItem WeaponItem ;
public InstantWeapon Weapon ;
public HullData HullData ;
public EquippedItem [ ] Markers ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
public EquippedItem BigMarker ;
public int2 [ ] BigMarkerCells ;
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
public EquippedItem LMarker ;
public int2 [ ] LMarkerCells ;
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
}
// A shooter off the origin (Cut 11.3), a target over a caller-supplied hull shape, a full-strength
// targeting system (Accuracy 1, Resolution huge, Tracking huge) so only the geometry under test moves the
// number, and an optional shield. markerCells equips the catalog's bare "Marker" Tool stub at each given
// interior cell -- returned in the same order -- for tests that need to name a specific cell (bow, stern).
private Engagement Build (
GameplaySettings settings , Shape hullShape , float precision ,
float velocity = 0 , float spread = 0 , float2 ? targetFacing = null ,
bool equipShield = false , float shieldCapacity = 30 ,
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
int2 [ ] markerCells = null , float accuracy = 1f , float penetration = 0 , bool equipBigMarker = false ,
// S1 fix batch (Hands, 2026-09-25): a caller-chosen zone name -- Cut 6.1's own convention is one fixed
// name so a fixture is replayable byte-for-byte, but Soul's own crash repros (Zone.CombatSeed is
// StableHash(name)) each need their OWN exact name to reproduce a specific die roll.
string zoneName = "Cut12" ,
// S7 fix batch (Hands, 2026-09-25): Tracking authored through Build, like Precision and Accuracy already
// are, instead of a caller patching PendingShot.Tracking after Fire (TheHudEstimateIsTheCommitPrice's own
// former shape) -- one authoring path for every frozen shooter stat.
float tracking = 100000f ,
// S8 fix batch (Hands, 2026-09-25): an L-shaped (3-cell, not 2x2) Tool item -- its true centroid differs
// from both any single cell and the bounding-box centre a mutant could substitute (N11), which a
// rectangular BigMarker cannot distinguish.
bool equipLMarker = false )
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
{
var hullData = new HullData
{
Name = "Hull" , HullType = HullType . Ship , Shape = hullShape , Durability = 100000 , Mass = 1000 , Armor = 1000 ,
Hardpoints = { new HardpointData { Type = HardpointType . Sensors , Position = new int2 ( 0 , 0 ) , Shape = new Shape ( ) } }
} ;
// The shooter's own hull is a fixed, always-roomy shape -- distinct from the target's caller-supplied
// hullShape, which some tests (BroadsideIsEasierThanHeadOn's 2-wide hull, TheSigmaFloorHoldsAtHalfACell's
// 1-wide lane) deliberately make too narrow to have any interior at all, so a Tool item (the shooter's
// own Targeting system) could never fit on it.
var shooterHullData = new HullData
{
Name = "ShooterHull" , HullType = HullType . Ship , Shape = SolidShape ( 5 , 5 ) , Durability = 100000 , Mass = 1000 ,
Hardpoints = { new HardpointData { Type = HardpointType . Sensors , Position = new int2 ( 0 , 0 ) , Shape = new Shape ( ) } }
} ;
var cache = AetheriaStores . Open ( Catalog + Guid . NewGuid ( ) . ToString ( "N" ) , catalogWritable : true ) ;
_openCaches . Add ( cache ) ;
cache . Upsert ( new TestCatalogGlobal { Name = "Temperament" } ) ;
cache . Upsert ( hullData ) ;
cache . Upsert ( shooterHullData ) ;
cache . Upsert ( new GearData
{
Name = "Gun" , Hardpoint = HardpointType . Sensors , Shape = new Shape ( ) , Durability = 1 ,
MinimumTemperature = - 1000 , MaximumTemperature = 1000 , OptimalTemperature = 0 , PlateauWidth = 2000 ,
Behaviors = { new InstantWeaponData
{
Damage = Constant ( 5 ) , Range = Constant ( 1000 ) , MinRange = Constant ( 0 ) ,
Velocity = Constant ( velocity ) , Spread = Constant ( spread ) , DamageSpread = Constant ( 0 ) ,
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
Penetration = Constant ( penetration ) , Count = Constant ( 1 ) , BurstTime = Constant ( 0 ) , Cooldown = Constant ( 1000 ) ,
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
DamageCurve = new BezierCurve { Keys = new [ ] { float4 ( 0 , 1 , 0 , 0 ) , float4 ( 1 , 1 , 0 , 0 ) } }
} }
} ) ;
cache . Upsert ( new GearData
{
Name = "Targeting" , Hardpoint = HardpointType . Tool , Shape = new Shape ( ) , Durability = 1 ,
MinimumTemperature = - 1000 , MaximumTemperature = 1000 , OptimalTemperature = 0 , PlateauWidth = 2000 ,
Behaviors = { new TargetingSystemData
{
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
Accuracy = Constant ( accuracy ) , Resolution = Constant ( 1000f ) , Precision = Constant ( precision ) , Tracking = Constant ( tracking )
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
} }
} ) ;
cache . Upsert ( new GearData
{
Name = "Reactor" , Hardpoint = HardpointType . Tool , Shape = new Shape ( ) , Durability = 10 ,
MinimumTemperature = - 1000 , MaximumTemperature = 1000 , OptimalTemperature = 0 , PlateauWidth = 2000 ,
Behaviors = { new ReactorData { Charge = Constant ( 1000 ) , Efficiency = Constant ( 1 ) , OverloadEfficiency = Constant ( 1 ) , ThrottlingFactor = Constant ( 2 ) } }
} ) ;
cache . Upsert ( new GearData
{
Name = "Shield" , Hardpoint = HardpointType . Tool , Shape = new Shape ( ) , Durability = 10 ,
MinimumTemperature = - 1000 , MaximumTemperature = 1000 , OptimalTemperature = 0 , PlateauWidth = 2000 ,
Behaviors = { new ShieldData { Efficiency = Constant ( 1 ) , EnergyUsage = Constant ( 1 ) , Capacity = Constant ( shieldCapacity ) , RefillDuration = Constant ( . 01f ) , RestoreDuration = Constant ( . 01f ) } }
} ) ;
cache . Upsert ( new GearData
{
Name = "Marker" , Hardpoint = HardpointType . Tool , Shape = new Shape ( ) , Durability = 1000000 ,
MinimumTemperature = - 1000 , MaximumTemperature = 1000 , OptimalTemperature = 0 , PlateauWidth = 2000
} ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
// F7 (Soul's fix batch, 2026-09-24): a 2x2 Tool item -- every "Marker" fixture elsewhere is the
// catalog's 1x1 stub, where ResolveAimPoint's average (sum of cells / cell count) can't be
// distinguished from summing then multiplying, or from just reading the first cell.
var bigMarkerShape = new Shape ( 2 , 2 ) ;
foreach ( var c in bigMarkerShape . AllCoordinates ) bigMarkerShape [ c ] = true ;
cache . Upsert ( new GearData
{
Name = "BigMarker" , Hardpoint = HardpointType . Tool , Shape = bigMarkerShape , Durability = 1000000 ,
MinimumTemperature = - 1000 , MaximumTemperature = 1000 , OptimalTemperature = 0 , PlateauWidth = 2000
} ) ;
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
// S8 fix batch: an L-tromino (3 of a 2x2's 4 cells) -- its centroid ((0,0)+(1,0)+(0,1))/3 = (1/3,1/3) is
// neither any one of its own cells nor the bounding box's centre (.5,.5), so a fixture built from it can
// tell a true centroid apart from both.
var lMarkerShape = new Shape ( 2 , 2 ) { [ new int2 ( 0 , 0 ) ] = true , [ new int2 ( 1 , 0 ) ] = true , [ new int2 ( 0 , 1 ) ] = true } ;
cache . Upsert ( new GearData
{
Name = "LMarker" , Hardpoint = HardpointType . Tool , Shape = lMarkerShape , Durability = 1000000 ,
MinimumTemperature = - 1000 , MaximumTemperature = 1000 , OptimalTemperature = 0 , PlateauWidth = 2000
} ) ;
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
cache . FlushAsync ( ) . Wait ( ) ;
var ledger = new ProvenanceLedger ( ) ;
for ( var i = 1 ; i < = 24 ; i + + ) ledger . Lots [ i ] = new Lot { Origin = new Attributed ( ) , Quality = 1 } ;
var items = new ItemManager ( cache , ledger , settings , _ = > { } ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
// Cut 6.1's own convention (FireControlCut6Tests.cs:62-63): every engagement shares one fixed
// GalaxyZone name, so every Zone this fixture builds shares one CombatSeed and every statistical test
// is replayable byte-for-byte across runs. A GUID-suffixed name here (this file's own earlier defect)
// reseeded the die on every run, which is exactly what made TurningArmourIntoTheShotTakesItOnTheArmour
// flaky (F2, Soul's fix batch, 2026-09-24).
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
var zone = new Zone ( items , new PlanetSettings ( ) , new ZonePack ( ) , new GalaxyZone { Name = zoneName , Owner = null } , null ) ;
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
EquippableItem Make ( string name , int lot , float durability ) = > new EquippableItem
{
Data = items . ItemData . RefOf < ItemData > ( items . ItemData . GetByName < GearData > ( name ) ) , Durability = durability , Lot = lot
} ;
var lot = 1 ;
var hullRef = items . ItemData . RefOf < ItemData > ( items . ItemData . GetByName < HullData > ( "Hull" ) ) ;
var shooterHullRef = items . ItemData . RefOf < ItemData > ( items . ItemData . GetByName < HullData > ( "ShooterHull" ) ) ;
var shooter = new Ship ( items , zone , new EquippableItem { Data = shooterHullRef , Durability = 1000000 , Lot = lot + + } , new EntitySettings ( ) ) ;
var gun = Make ( "Gun" , lot + + , 1 ) ;
Assert . True ( shooter . TryEquip ( gun , new int2 ( 0 , 0 ) ) ) ;
var weaponItem = shooter . Equipment . Single ( x = > x . EquippableItem = = gun ) ;
var weapon = ( InstantWeapon ) weaponItem . Behaviors . Single ( b = > b is Weapon ) ;
var targeting = Make ( "Targeting" , lot + + , 1 ) ;
Assert . True ( shooter . TryFindSpace ( targeting , out var tpos ) ) ;
Assert . True ( shooter . TryEquip ( targeting , tpos ) ) ;
var target = new Ship ( items , zone , new EquippableItem { Data = hullRef , Durability = 1000000 , Lot = lot + + } , new EntitySettings ( ) ) ;
// A bystander item on the Hardpoint slot every hull shape has (position (0,0) needs no Shrink-interior,
// unlike a Tool) -- Entity.Update's `foreach (var item in _orderedEquipment)` NREs if an entity was
// never equipped with anything at all (_orderedEquipment stays null until TryEquip first assigns it),
// the same reason FireControlCut9Tests.BuildLockScenario equips its own bystander with a token gun.
var targetGun = Make ( "Gun" , lot + + , 1 ) ;
Assert . True ( target . TryEquip ( targetGun , new int2 ( 0 , 0 ) ) ) ;
var markers = markerCells ? . Select ( cell = >
{
var m = Make ( "Marker" , lot + + , 1000000 ) ;
Assert . True ( target . TryEquip ( m , cell ) , $"marker must fit at {cell}" ) ;
return target . Equipment . Single ( x = > x . EquippableItem = = m ) ;
} ) . ToArray ( ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
EquippedItem bigMarker = null ;
int2 [ ] bigMarkerCells = null ;
if ( equipBigMarker )
{
var bm = Make ( "BigMarker" , lot + + , 1000000 ) ;
Assert . True ( target . TryFindSpace ( bm , out var bmPos ) ) ;
Assert . True ( target . TryEquip ( bm , bmPos ) ) ;
bigMarker = target . Equipment . Single ( x = > x . EquippableItem = = bm ) ;
bigMarkerCells = hullShape . Coordinates . Where ( v = > target . GearOccupancy [ v . x , v . y ] = = bigMarker ) . ToArray ( ) ;
Assert . Equal ( 4 , bigMarkerCells . Length ) ; // fixture precondition: the full 2x2 footprint landed
}
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
EquippedItem lMarker = null ;
int2 [ ] lMarkerCells = null ;
if ( equipLMarker )
{
var lm = Make ( "LMarker" , lot + + , 1000000 ) ;
Assert . True ( target . TryFindSpace ( lm , out var lmPos ) ) ;
Assert . True ( target . TryEquip ( lm , lmPos ) ) ;
lMarker = target . Equipment . Single ( x = > x . EquippableItem = = lm ) ;
lMarkerCells = hullShape . Coordinates . Where ( v = > target . GearOccupancy [ v . x , v . y ] = = lMarker ) . ToArray ( ) ;
Assert . Equal ( 3 , lMarkerCells . Length ) ; // fixture precondition: the full 3-cell L footprint landed
}
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
if ( equipShield )
{
var reactor = Make ( "Reactor" , lot + + , 10 ) ;
Assert . True ( target . TryFindSpace ( reactor , out var rpos ) ) ;
Assert . True ( target . TryEquip ( reactor , rpos ) ) ;
var shield = Make ( "Shield" , lot + + , 10 ) ;
Assert . True ( target . TryFindSpace ( shield , out var spos ) ) ;
Assert . True ( target . TryEquip ( shield , spos ) ) ;
}
zone . Entities . Add ( shooter ) ;
zone . Entities . Add ( target ) ;
shooter . Activate ( ) ;
target . Activate ( ) ;
// Cut 11.3: the shooter sits off the origin -- a shooter at float3.zero makes `target - source` and
// `target + source` the same vector, hiding a whole class of sign mutants.
shooter . Position = float3 ( 37 , 0 , - 11 ) ;
target . Position = float3 ( 37 , 0 , 89 ) ; // 100 units straight ahead of the shooter
if ( targetFacing ! = null ) target . Direction = targetFacing . Value ;
shooter . Target . Value = target ;
shooter . EntityInfoGathered [ target ] = 1f ;
shooter . SetIff ( target , true ) ;
if ( equipShield )
{
target . Shield . Item . Enabled . Value = true ;
for ( var i = 0 ; i < 20 ; i + + ) zone . Update ( . 1f ) ; // charge from empty (Cut 11's own Charge() convention)
}
// Cut 11.3: fire times are nonzero -- warm up past a bare zone.Update(0f) so FireTime is never 0.
zone . Update ( 1f ) ;
return new Engagement
{
Items = items , Zone = zone , Shooter = shooter , Target = target , WeaponItem = weaponItem , Weapon = weapon ,
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
HullData = hullData , Markers = markers , BigMarker = bigMarker , BigMarkerCells = bigMarkerCells ,
LMarker = lMarker , LMarkerCells = lMarkerCells
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
} ;
}
private static List < ShotOutcome > FireMany ( Engagement e , int count )
{
var outcomes = new List < ShotOutcome > ( ) ;
e . Zone . ShotResolved . Subscribe ( o = > outcomes . Add ( o ) ) ;
for ( var i = 0 ; i < count ; i + + )
{
FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
e . Zone . Update ( . 01f ) ;
}
return outcomes ;
}
// Independent reimplementation of FireControl.Lane's slab test, from the spec's own formulas, over the
// hull's public Shape.Coordinates -- used to check production's placement against an outcome's own frozen
// Bearing/Lateral without calling FireControl.Lane itself.
private static int2 NearestOccupiedCellAtLateral ( HullData hull , float2 bearing , float lateral )
{
var ell = float2 ( - bearing . y , bearing . x ) ;
var h = ( Math . Abs ( ell . x ) + Math . Abs ( ell . y ) ) / 2f ;
int2 ? best = null ;
var bestEntry = float . PositiveInfinity ;
foreach ( var c in hull . Shape . Coordinates )
{
var centre = dot ( ( float2 ) c , ell ) ;
if ( lateral < centre - h | | lateral > = centre + h ) continue ;
var cf = ( float2 ) c ;
var entry = float . NegativeInfinity ;
var exit = float . PositiveInfinity ;
var valid = true ;
foreach ( var axis in new [ ] { 0 , 1 } )
{
var bAxis = axis = = 0 ? bearing . x : bearing . y ;
var cAxis = axis = = 0 ? cf . x : cf . y ;
var val = axis = = 0 ? lateral * ell . x : lateral * ell . y ;
var lo = cAxis - . 5f ;
var hi = cAxis + . 5f ;
if ( Math . Abs ( bAxis ) < 1e-9f )
{
if ( val < lo | | val > hi ) valid = false ;
continue ;
}
var t1 = ( lo - val ) / bAxis ;
var t2 = ( hi - val ) / bAxis ;
entry = Math . Max ( entry , Math . Min ( t1 , t2 ) ) ;
exit = Math . Min ( exit , Math . Max ( t1 , t2 ) ) ;
}
if ( ! valid | | entry > exit ) continue ;
if ( entry < bestEntry )
{
bestEntry = entry ;
best = c ;
}
}
Assert . True ( best ! = null , "fixture: no occupied cell reached at this lateral offset" ) ;
return best . Value ;
}
2026-09-24 21:25:15 +02:00
// Stryker survivors on this cut's own changed lines (2026-09-24 run): TravelDirection's point-blank
// ternary (both directions), and Fire's target-null guard around it, were never actually exercised at a
// shooter/target pair where the real direction could not coincidentally equal the (0,1) fallback. Kills:
// forcing either ternary branch; skipping the null-target guard (which would NRE on PredictedIntercept).
[Fact]
public void TravelDirectionMatchesThePredictedInterceptAndFallsBackAtPointBlank ( )
{
var e = Build ( TestSettings ( ) , SolidShape ( 5 , 5 ) , precision : 1f ) ;
// A diagonal placement -- neither axis-aligned with the fallback (0,1), so a mutant that always
// returns the fallback (or always skips the null-target guard) cannot pass by coincidence.
e . Shooter . Position = float3 ( 10 , 0 , - 5 ) ;
e . Target . Position = float3 ( 40 , 0 , 65 ) ;
var expected = normalize ( ( e . Target . Position - e . Shooter . Position ) . xz ) ;
var shotId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
var pending = e . Zone . PendingShots . Single ( s = > s . ShotId = = shotId ) ;
Assert . Equal ( expected . x , pending . TravelDirection . x , 4 ) ;
Assert . Equal ( expected . y , pending . TravelDirection . y , 4 ) ;
// Point-blank: shooter and target at the exact same position (weapon Velocity 0, so PredictedIntercept
// returns target.Position exactly) -- TravelDirection must fall back rather than normalize a zero
// vector into NaN.
e . Target . Position = e . Shooter . Position ;
var pointBlankId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
var pointBlank = e . Zone . PendingShots . Single ( s = > s . ShotId = = pointBlankId ) ;
Assert . Equal ( 0f , pointBlank . TravelDirection . x ) ;
Assert . Equal ( 1f , pointBlank . TravelDirection . y ) ;
// No target: Fire's own null-target branch must use the fallback directly, not call TravelDirection
// (which reads target.Position and would NRE).
e . Shooter . Target . Value = null ;
var noTargetId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
var noTarget = e . Zone . PendingShots . Single ( s = > s . ShotId = = noTargetId ) ;
Assert . Equal ( 0f , noTarget . TravelDirection . x ) ;
Assert . Equal ( 1f , noTarget . TravelDirection . y ) ;
}
// Stryker survivor: diagnostic.PFire (Accuracy * PSensor, an optional presentation field) was never
// checked at a PSensor strictly between 0 and 1, so Accuracy * PSensor and Accuracy / PSensor (the
// mutant) were indistinguishable at PSensor == 1 (the fixture's usual full-reveal convention). Resolution
// 1000 gives an razor-thin demand window (ceiling ~= .1009 against a .1 threshold); info exactly halfway
// through it gives PSensor exactly .5.
[Fact]
public void DiagnosticPFireIsAccuracyTimesPSensor ( )
{
var e = Build ( TestSettings ( ) , SolidShape ( 6 , 12 ) , precision : . 6f ) ;
e . Shooter . EntityInfoGathered [ e . Target ] = . 10045f ; // halfway through the [.1, .1009] demand window
var d = FireControl . Inspect ( e . Weapon , e . Shooter , e . Target ) ;
Assert . True ( d . PSensor > . 05f & & d . PSensor < . 95f , $"fixture: PSensor ({d.PSensor}) must be strictly between 0 and 1" ) ;
Assert . Equal ( d . Accuracy * d . PSensor , d . PFire , 4 ) ;
}
// Stryker survivors: CommitProbability's own product (PFire * pDeviation * PSpread * pOnHull) was only
// ever exercised with pDeviation exactly 1 (a stationary target relative to its fire-time projection),
// which makes "* pDeviation" and "/ pDeviation" (and boundary flips around 0) indistinguishable. A
// synthetic PendingShot (bypassing Fire, the same convention FireControlCut9Tests.FireSynthetic uses)
// pins every factor to a distinct, non-1 value and checks the exact product against an independent
// recomputation.
[Fact]
public void CommitProbabilityIsTheExactProductOfItsFourFactors ( )
{
var e = Build ( TestSettings ( ) , SolidShape ( 7 , 9 ) , precision : . 5f , spread : 6f ) ;
var shotId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
var pending = e . Zone . PendingShots . Single ( s = > s . ShotId = = shotId ) ;
// A jink between fire and commit gives pDeviation a real, non-1 value distinct from PFire and PSpread.
pending . FireTime = e . Zone . Time - 1f ;
pending . PFire = . 8f ;
pending . Tracking = 20f ;
e . Zone . PendingShots [ e . Zone . PendingShots . FindIndex ( s = > s . ShotId = = shotId ) ] = pending ;
e . Target . Position + = float3 ( 6 , 0 , 0 ) ; // jink after the (frozen) fire-time projection
var p = FireControl . CommitProbability ( pending , e . Zone . Time , out var sil ) ;
var pDeviation = FireControl . DeviationProbability ( pending , e . Zone . Time , out var deviation ) ;
Assert . True ( deviation > 0f , "fixture: the jink must actually move the target off its projection" ) ;
Assert . True ( pDeviation > . 05f & & pDeviation < . 95f , $"fixture: pDeviation ({pDeviation}) must be strictly between 0 and 1" ) ;
Assert . True ( sil . POnHull > . 05f & & sil . POnHull < . 95f , $"fixture: pOnHull ({sil.POnHull}) must be strictly between 0 and 1" ) ;
var pSpread = e . HullData . Shape . Width > 0
? Math . Min ( 1f , ( float ) ( 180.0 / Math . PI * Math . Atan ( . 5 * sil . Span / pending . FireRange ) ) / ( pending . Spread / 2f ) )
: 1f ;
var expected = pending . PFire * pDeviation * pSpread * sil . POnHull ;
Assert . Equal ( expected , p , 4 ) ;
}
2026-09-25 03:24:13 +02:00
// :~483 fix batch (Hands, 2026-09-25): `if (p <= 0f) return 0f;` in CommitProbability is a guard against
// building a silhouette for a shot that cannot hit -- Stryker's own `p < 0` boundary mutant (excluding only
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
// the single value p == 0 exactly) survived because nothing exercised the guard at exactly 0.
// 12.3 fix batch correction: the previous version of this test set `shot.PFire = 0f` on a pending shot by
// hand, then asserted `sil.Intervals == null` -- a shape assertion on CommitProbability's own struct
// default, not a behaviour a bug could actually violate (any caller reading `sil` after a real miss reads
// the same default regardless of whether a Silhouette was built and discarded). Made behavioural per the
// 12.2 postmortem: PFire == 0 is now produced through the real gate (closing the shooter's own
// VisibleEntities, the same way S4's InvisibleTargetStillResolvesAtItsRealFlightTime does), and the claim
// "no Silhouette was ever built" is checked the only way that is actually true of a function's own
// behaviour: it allocates nothing. The `p < 0` mutant lets p == 0 fall through to build a real Silhouette
// (Array.Sort, ResolveAimPoint, an owned Interval[]), which is not zero-allocation -- this test kills it
// the same way S3's own zero-allocation pins do, not by re-reading a field the mutant never touches.
2026-09-25 03:24:13 +02:00
[Fact]
public void ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette ( )
{
var e = Build ( TestSettings ( ) , SolidShape ( 5 , 5 ) , precision : 1f ) ;
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
e . Shooter . VisibleEntities . Remove ( e . Target ) ; // closes the real gate -- PFire == 0 through the actual path
2026-09-25 03:24:13 +02:00
var shotId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
var shot = e . Zone . PendingShots . Single ( s = > s . ShotId = = shotId ) ;
Assert . Equal ( 0f , shot . PFire ) ; // fixture precondition: the gate is actually closed
for ( var i = 0 ; i < 50 ; i + + ) FireControl . CommitProbability ( shot , e . Zone . Time , out _ ) ; // JIT warm-up
2026-09-25 03:24:13 +02:00
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
var before = GC . GetAllocatedBytesForCurrentThread ( ) ;
2026-09-25 03:24:13 +02:00
var p = FireControl . CommitProbability ( shot , e . Zone . Time , out var sil ) ;
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
var allocated = GC . GetAllocatedBytesForCurrentThread ( ) - before ;
2026-09-25 03:24:13 +02:00
Assert . Equal ( 0f , p ) ;
Assert . Equal ( 0 , sil . Count ) ;
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
Assert . Equal ( 0 , allocated ) ; // no Silhouette was built: Array.Sort/ResolveAimPoint never ran
2026-09-25 03:24:13 +02:00
ShotOutcome outcome = null ;
using var r = e . Zone . ShotResolved . Subscribe ( o = > outcome = o ) ;
e . Zone . Update ( . 01f ) ;
Assert . NotNull ( outcome ) ;
Assert . False ( outcome . Hit , "fixture: PFire 0 must commit as a clean miss" ) ;
}
2026-09-25 03:14:15 +02:00
// S4 fix batch (Hands, 2026-09-25): F8 (Soul's earlier fix batch, 2026-09-24) moved range/flightTime
// computation ahead of the visibility gate, so Fire's own flight time reflects the real distance even when
// PFire prices the shot at 0 -- but nothing behavioural pinned that. N4 (Soul's mutation set: "range after
// visibility gate", reverting F8) survived every existing test. A shot at a target the shooter cannot
// currently see must still resolve, as a miss (PFire gates it to 0), at the arrival time the real
// range/velocity imply -- not immediately (range 0) and not never (flight time 0 means instant resolve).
[Fact]
public void InvisibleTargetStillResolvesAtItsRealFlightTime ( )
{
var e = Build ( TestSettings ( ) , SolidShape ( 5 , 5 ) , precision : 1f , velocity : 20 ) ; // range 100 -> 5s flight
e . Shooter . VisibleEntities . Remove ( e . Target ) ;
var shotId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
var pending = e . Zone . PendingShots . Single ( s = > s . ShotId = = shotId ) ;
Assert . Equal ( 0f , pending . PFire ) ; // fixture precondition: the visibility gate is actually closed
var expectedArrival = pending . FireTime + 100f / 20f ; // the real range (100) over the real velocity (20)
Assert . Equal ( expectedArrival , pending . ArrivalTime , 3 ) ;
ShotOutcome outcome = null ;
using var r = e . Zone . ShotResolved . Subscribe ( o = > outcome = o ) ;
e . Zone . Update ( 4.9f ) ;
Assert . Null ( outcome ) ; // not yet: N4's bug (range 0) would have resolved this instantly
e . Zone . Update ( . 2f ) ; // past the real 5s arrival
Assert . NotNull ( outcome ) ;
Assert . False ( outcome . Hit , "fixture: PFire 0 (not visible) must resolve as a miss" ) ;
}
2026-09-24 21:25:15 +02:00
// Stryker survivors: ResolveAimPoint's fallback (aimed but currently occupying zero cells -> hull centre
// of mass) and its multi-cell average were both untested -- every existing marker fixture uses the
// catalog's 1x1 "Marker" stub, where sum/length and sum*length agree at length 1, and no fixture ever
// destroys or unequips an aimed item before firing.
[Fact]
public void AimPointFallsBackWhenTheAimedItemNoLongerOccupiesAnyCell ( )
{
var aim = new int2 ( 2 , 2 ) ;
var e = Build ( TestSettings ( ) , SolidShape ( 6 , 6 ) , precision : 1000f , markerCells : new [ ] { aim } ) ;
var aimed = e . Markers [ 0 ] ;
Assert . True ( e . Shooter . TrySelectTargetItem ( aimed ) ) ;
// The aimed item is still equipped (Entity.TryUnequip refuses on an active entity, and reveal/aim
// selection reads Equipment, not GearOccupancy) but no longer occupies any hull cell -- the shape a
// destroyed or relocated item's footprint would leave. CellsOf then returns an empty (non-null) array,
// and ResolveAimPoint must fall back to the hull's own centre of mass rather than average zero cells
// (0/0 = NaN).
e . Target . GearOccupancy [ aim . x , aim . y ] = null ;
var outcomes = FireMany ( e , 400 ) ;
var hits = outcomes . Where ( o = > o . Hit ) . ToList ( ) ;
Assert . True ( hits . Count > 50 , $"expected a healthy number of hits, got {hits.Count}" ) ;
foreach ( var outcome in hits )
Assert . True ( e . HullData . Shape [ outcome . Cell ] , $"hit landed on {outcome.Cell}, off the hull's own schematic -- the aim point fallback produced NaN" ) ;
}
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
// F7 (Soul's fix batch, 2026-09-24): ResolveAimPoint's average was only ever exercised with a 1x1 item
// (every other fixture's "Marker"), where sum/length and sum*length agree and "first cell" and "average"
// are the same cell. A 2x2 "BigMarker" gives a genuine 4-cell footprint: its true centroid is checked two
// ways -- pOnHull does not collapse (the aim point is on the hull, not scaled off it by sum*length), and,
// from broadside, the empirical mean lateral offset of hits centres on the real centroid's own lane, not
// on any single one of its four cells.
[Fact]
public void AimingAtAnItemBiggerThanOneCellCentresOnItsCentroid ( )
{
var e = Build ( TestSettings ( ) , SolidShape ( 9 , 9 ) , precision : 1f , targetFacing : float2 ( 1 , 0 ) , equipBigMarker : true ) ;
Assert . True ( e . Shooter . TrySelectTargetItem ( e . BigMarker ) ) ;
var travelDirection = FireControl . TravelDirection ( e . Weapon , e . Shooter , e . Target ) ;
var bearing = normalize ( e . Target . ToSchematic ( travelDirection ) ) ;
var ell = float2 ( - bearing . y , bearing . x ) ;
var centroid = e . BigMarkerCells . Aggregate ( float2 . zero , ( t , c ) = > t + ( float2 ) c ) / e . BigMarkerCells . Length ;
var expectedA = dot ( centroid , ell ) ;
// pOnHull must not collapse -- a sum*length aim point would be scaled several cells off the hull
// entirely, driving pOnHull toward zero even at Precision 1 (sigma 1, comfortably inside a 9x9 hull).
var hud = FireControl . Inspect ( e . Weapon , e . Shooter , e . Target ) ;
Assert . True ( hud . POnHull > . 3f , $"pOnHull ({hud.POnHull}) collapsed -- the aim point is likely off the hull" ) ;
var outcomes = FireMany ( e , 3000 ) ;
var hits = outcomes . Where ( o = > o . Hit ) . ToList ( ) ;
Assert . True ( hits . Count > 300 , $"expected a healthy number of hits, got {hits.Count}" ) ;
var empiricalMeanLateral = hits . Average ( o = > ( double ) o . Lateral ) ;
// Sigma is 1 cell; a "first cell only" aim point sits at least .5 cell off the true centroid on a 2x2
// footprint, well outside this tolerance for a few-hundred-shot mean.
Assert . True ( Math . Abs ( empiricalMeanLateral - expectedA ) < . 25 ,
$"empirical mean lateral offset {empiricalMeanLateral:F3} does not track the centroid's own lane {expectedA:F3}" ) ;
}
2026-09-25 03:21:40 +02:00
// S8 fix batch (Hands, 2026-09-25): the BigMarker test above only ever exercises a 2x2 (square) footprint,
// whose true centroid and bounding-box centre are the same point -- N11 (aim point = bounding-box centre
// instead of the cell average) survived because nothing could tell the two apart. An L-tromino (3 of a
// 2x2's 4 cells) makes them different points: this fixture pins the aim point against the exact centroid
// the test computes itself, and separately checks it against the wrong (bounding-box) candidate to prove
// the fixture actually distinguishes them.
[Fact]
public void AimingAtAnLShapedItemCentresOnItsCentroidNotItsBoundingBox ( )
{
// A diagonal facing (not axis-aligned) puts the L-shape's actual asymmetry -- its centroid sits off
// the bounding-box centre along the diagonal, not along either grid axis -- fully onto the lateral
// axis, instead of losing most of it to the axis a purely axis-aligned facing would leave unmeasured.
var e = Build ( TestSettings ( ) , SolidShape ( 9 , 9 ) , precision : 1f , targetFacing : normalize ( float2 ( 1 , 1 ) ) , equipLMarker : true ) ;
Assert . True ( e . Shooter . TrySelectTargetItem ( e . LMarker ) ) ;
var travelDirection = FireControl . TravelDirection ( e . Weapon , e . Shooter , e . Target ) ;
var bearing = normalize ( e . Target . ToSchematic ( travelDirection ) ) ;
var ell = float2 ( - bearing . y , bearing . x ) ;
var centroid = e . LMarkerCells . Aggregate ( float2 . zero , ( t , c ) = > t + ( float2 ) c ) / e . LMarkerCells . Length ;
var boundingBoxCentre = ( float2 ( e . LMarkerCells . Min ( c = > c . x ) , e . LMarkerCells . Min ( c = > c . y ) )
+ float2 ( e . LMarkerCells . Max ( c = > c . x ) , e . LMarkerCells . Max ( c = > c . y ) ) ) / 2f ;
Assert . True ( lengthsq ( centroid - boundingBoxCentre ) > . 01f , "fixture: the L-shape's centroid and bounding-box centre must actually differ" ) ;
var expectedA = dot ( centroid , ell ) ;
var wrongA = dot ( boundingBoxCentre , ell ) ;
Assert . True ( Math . Abs ( expectedA - wrongA ) > . 15f , "fixture: the two candidate aim points must project to distinguishably different lateral values" ) ;
var outcomes = FireMany ( e , 3000 ) ;
var hits = outcomes . Where ( o = > o . Hit ) . ToList ( ) ;
Assert . True ( hits . Count > 300 , $"expected a healthy number of hits, got {hits.Count}" ) ;
var empiricalMeanLateral = hits . Average ( o = > ( double ) o . Lateral ) ;
Assert . True ( Math . Abs ( empiricalMeanLateral - expectedA ) < . 1 ,
$"empirical mean lateral offset {empiricalMeanLateral:F3} (bounding-box centre would predict {wrongA:F3}) does not track the true centroid's own lane {expectedA:F3}" ) ;
}
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
// BroadsideIsEasierThanHeadOn: kills the bearing being ignored and the old max(W,H) bound. A 2x12 hull is
// much easier to hit broadside (its long axis facing the shot) than head-on (its short axis facing it).
[Fact]
public void BroadsideIsEasierThanHeadOn ( )
{
float PAt ( float2 facing )
{
var e = Build ( TestSettings ( ) , SolidShape ( 2 , 12 ) , precision : . 5f , spread : 4 , targetFacing : facing ) ;
return FireControl . Inspect ( e . Weapon , e . Shooter , e . Target ) . PSpread ;
}
float POnHullAt ( float2 facing )
{
var e = Build ( TestSettings ( ) , SolidShape ( 2 , 12 ) , precision : . 5f , targetFacing : facing ) ;
return FireControl . Inspect ( e . Weapon , e . Shooter , e . Target ) . POnHull ;
}
// The shooter fires from world +z toward the target (TravelDirection ~ world +z, since the target sits
// straight ahead). Facing (0,1) (bow toward the shooter, forward parallel to travel) puts the ship's
// SHORT axis (width 2) across the bearing -- head-on. Facing (1,0) puts its LONG axis (height 12)
// across the bearing -- broadside.
var headOnPSpread = PAt ( float2 ( 0 , 1 ) ) ;
var broadsidePSpread = PAt ( float2 ( 1 , 0 ) ) ;
Assert . True ( broadsidePSpread > headOnPSpread ,
$"broadside PSpread ({broadsidePSpread}) must exceed head-on PSpread ({headOnPSpread})" ) ;
var headOnPOnHull = POnHullAt ( float2 ( 0 , 1 ) ) ;
var broadsidePOnHull = POnHullAt ( float2 ( 1 , 0 ) ) ;
Assert . True ( broadsidePOnHull > headOnPOnHull ,
$"broadside pOnHull ({broadsidePOnHull}) must exceed head-on pOnHull ({headOnPOnHull})" ) ;
}
// TurningArmourIntoTheShotTakesItOnTheArmour (the ruling's own case): a slow shot at a target whose stern
// is exposed at Fire. The target turns its armoured bow to face the incoming shot (present the bow toward
// where the shot is travelling FROM) before the shot commits. Every hit must land on the bow row --
// Bearing timing reads the facing live, at Commit, not frozen at Fire.
[Fact]
public void TurningArmourIntoTheShotTakesItOnTheArmour ( )
{
var settings = TestSettings ( ) ;
// Flight time well above CommitHorizon (.5s): range 100 at velocity 20 -> 5s flight, commits at 4.5s.
var e = Build ( settings , SolidShape ( 3 , 11 ) , precision : . 4f , velocity : 20 , targetFacing : null ) ;
var outcomes = new List < ShotOutcome > ( ) ;
using var r = e . Zone . ShotResolved . Subscribe ( outcomes . Add ) ;
// Repeat the same fire-then-turn sequence several times (the roll can still miss outright) and check
// the set of hits, not one shot.
for ( var i = 0 ; i < 60 ; i + + )
{
var shotId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
var pending = e . Zone . PendingShots . Single ( s = > s . ShotId = = shotId ) ;
var travelDirection = pending . TravelDirection ;
// At Fire: bow faces the same way the shot travels -- the stern is exposed to the incoming shot.
e . Target . Direction = travelDirection ;
e . Zone . Update ( 1f ) ; // short of commit (4.5s)
// Before commit: turn the bow to face the incoming shot head-on.
e . Target . Direction = - travelDirection ;
e . Zone . Update ( 4f ) ; // past commit (4.5s), short of arrival (5s)
e . Zone . Update ( 2f ) ; // past arrival
}
var hits = outcomes . Where ( o = > o . Hit ) . ToList ( ) ;
Assert . True ( hits . Count > 10 , $"expected a healthy number of hits to check, got {hits.Count}" ) ;
foreach ( var outcome in hits )
Assert . True ( outcome . Cell . y > = 5.5f , $"expected every hit to land on the bow row (y>=5.5), got cell {outcome.Cell}" ) ;
}
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
// S1 (Soul's second pass, 2026-09-25): at a non-axis-aligned bearing, a lateral draw exactly at an
// interval's Lo used to touch only a cell's corner -- Lane's own shadow prefilter admitted the cell but the
// independent SlabAlongB slab test rejected it on float rounding, Lane returned zero cells, and Commit's
// own guard threw InvalidOperationException out of Zone.Update (R3 violation: a hit that already passed
// its roll must always land on metal). Soul reproduced this live, not through reflection into a private
// method: zone name Cut12-edge-68442709 (StableHash seeds Zone.CombatSeed, so this exact name reproduces
// the exact die roll), a 7x9 hull, Precision 2, target facing 1 degree, shot 1. Fixed by deleting the
// second, disagreeing gate (this fix batch's FireControl.cs change) -- this pins the exact repro against a
// real Commit, not a reflection call, and checks the placement lands where the roll priced it.
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
[Fact]
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
public void ShotAtBearingLoTouchingOnlyACellCornerHitsMetal ( )
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
{
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
var e = Build ( TestSettings ( ) , SolidShape ( 7 , 9 ) , precision : 2f , targetFacing : Deg ( 1 ) , zoneName : "Cut12-edge-68442709" ) ;
ShotOutcome outcome = null ;
using var r = e . Zone . ShotResolved . Subscribe ( o = > outcome = o ) ;
FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
e . Zone . Update ( . 01f ) ;
Assert . NotNull ( outcome ) ;
Assert . True ( outcome . Hit , "fixture: this exact seed must roll a hit (this is the repro, not a general property)" ) ;
Assert . True ( e . HullData . Shape [ outcome . Cell ] , $"hit landed on {outcome.Cell}, not on the hull's own schematic" ) ;
var ell = float2 ( - outcome . Bearing . y , outcome . Bearing . x ) ;
var h = ( Math . Abs ( ell . x ) + Math . Abs ( ell . y ) ) / 2f ;
var centre = dot ( ( float2 ) outcome . Cell , ell ) ;
Assert . True ( outcome . Lateral > = centre - h & & outcome . Lateral < centre + h ,
$"hit cell {outcome.Cell}'s own projected lateral interval [{centre - h},{centre + h}) does not contain the drawn Lateral {outcome.Lateral}" ) ;
}
// S1: the same class of empty-lane crash on the ruling's own TurningArmourIntoTheShotTakesItOnTheArmour
// geometry (3x11 hull, velocity 20, Precision .4, the fire-then-turn sequence that fixture uses) --
// Soul's search found a zone name (Cut12-edge-64078070) whose draw lands on an interval edge after the
// target turns its bow into the shot.
[Fact]
public void ShotAtBearingLoOnTurningArmourHitsMetal ( )
{
var e = Build ( TestSettings ( ) , SolidShape ( 3 , 11 ) , precision : . 4f , velocity : 20 , zoneName : "Cut12-edge-64078070" ) ;
var shotId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
var pending = e . Zone . PendingShots . Single ( s = > s . ShotId = = shotId ) ;
var travelDirection = pending . TravelDirection ;
// At Fire: bow faces the same way the shot travels (stern exposed). Before commit: turn the bow to
// face the incoming shot head-on, TurningArmourIntoTheShotTakesItOnTheArmour's own sequence.
e . Target . Direction = travelDirection ;
e . Zone . Update ( 1f ) ;
e . Target . Direction = - travelDirection ;
ShotOutcome outcome = null ;
using var r = e . Zone . ShotResolved . Subscribe ( o = > outcome = o ) ;
e . Zone . Update ( 4f ) ; // past commit (4.5s), short of arrival (5s)
e . Zone . Update ( 2f ) ; // past arrival
Assert . NotNull ( outcome ) ;
Assert . True ( outcome . Hit , "fixture: this exact seed/sequence must roll a hit (this is the repro, not a general property)" ) ;
Assert . True ( e . HullData . Shape [ outcome . Cell ] , $"hit landed on {outcome.Cell}, not on the hull's own schematic" ) ;
Assert . True ( outcome . Cell . y > = 5.5f , $"expected the hit to land on the bow row (y>=5.5), got cell {outcome.Cell}" ) ;
var ell = float2 ( - outcome . Bearing . y , outcome . Bearing . x ) ;
var h = ( Math . Abs ( ell . x ) + Math . Abs ( ell . y ) ) / 2f ;
var centre = dot ( ( float2 ) outcome . Cell , ell ) ;
Assert . True ( outcome . Lateral > = centre - h & & outcome . Lateral < centre + h ,
$"hit cell {outcome.Cell}'s own projected lateral interval [{centre - h},{centre + h}) does not contain the drawn Lateral {outcome.Lateral}" ) ;
}
// S1 sweep: across many bearings and precisions, on hulls including a non-convex (holed) one, Lane must
// find at least one occupied cell for every lateral offset at an interval's own Lo, and just below its Hi
// -- the exact edges LateralDraw's own half-open clamp can produce (u=0 draws Lo; u near 1 draws just
// below Hi). An empty lane at either edge is precisely the class of crash S1 fixed; this is the general
// property the two named-seed repros above are specific instances of.
[Fact]
public void LaneNeverEmptiesAtAnIntervalEdge ( )
{
var shapes = new [ ] { SolidShape ( 7 , 9 ) , SolidShape ( 3 , 11 ) , HoledShape ( 7 , 7 ) } ;
var precisions = new [ ] { . 3f , . 5f , 1f , 2f } ;
var failures = new List < string > ( ) ;
foreach ( var shape in shapes )
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
{
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
var hull = new HullData { Name = "Sweep" , HullType = HullType . Ship , Shape = shape } ;
var buffer = new LaneCell [ shape . Coordinates . Length ] ;
for ( var deg = 0 ; deg < 360 ; deg + = 3 )
{
var b = Deg ( deg ) ;
foreach ( var precision in precisions )
{
var sil = FireControl . Silhouette ( null , hull , null , b , precision ) ;
for ( var k = 0 ; k < sil . Count ; k + + )
{
var iv = sil . Intervals [ k ] ;
if ( FireControl . Lane ( hull , b , iv . Lo , buffer ) = = 0 )
failures . Add ( $"{shape.Width}x{shape.Height} deg={deg} p={precision} k={k} at Lo={iv.Lo:R}" ) ;
var justBelowHi = Math . Max ( iv . Lo , iv . Hi - 1e-4f - 1e-5f ) ;
if ( FireControl . Lane ( hull , b , justBelowHi , buffer ) = = 0 )
failures . Add ( $"{shape.Width}x{shape.Height} deg={deg} p={precision} k={k} just below Hi={iv.Hi:R}" ) ;
}
}
}
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
}
Cut 12.2 Soul fix batch 2, S1: behavioural tests, not reflection, for the shadow fix
Replaces LateralDrawIsHalfOpenAtTheShadowsTopEdge (reflection into the private
LateralDraw) with:
- ShotAtBearingLoTouchingOnlyACellCornerHitsMetal and
ShotAtBearingLoOnTurningArmourHitsMetal, Soul's two named-seed crash repros
(Cut12-edge-68442709 and Cut12-edge-64078070 -- StableHash(zone name) seeds
Zone.CombatSeed, so the exact name reproduces the exact die roll) run
through the real public Fire/Zone.Update path, asserting the shot commits a
hit whose cell's own projected lateral interval contains the drawn Lateral,
instead of throwing.
- LaneNeverEmptiesAtAnIntervalEdge, the general sweep: every interval's Lo and
the point just below its own half-open Hi, across several bearings,
precisions and hulls (including a non-convex, holed one), must never return
an empty lane from FireControl.Lane.
Build gained a zoneName parameter (Cut 6.1's one-fixed-name convention doesn't
serve a repro that needs an exact CombatSeed) and a tracking parameter and an
equipLMarker fixture, both used by later findings in this batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:06:44 +02:00
Assert . True ( failures . Count = = 0 , $"{failures.Count} empty lanes at an interval edge, e.g.: {string.Join(" ; ", failures.Take(5))}" ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
}
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
// Carried into 12.3's brief (12.2 Soul pass 3): pins Lane's own seam contract directly, against a 2x1 hull
// -- the smallest fixture where a seam actually falls between two occupied cells. At b=(0,1), ell=(-1,0),
// so cell (0,0)'s shadow is [-.5,.5) and cell (1,0)'s is [.5,1.5): s=-0.5 must land only in (0,0)'s column,
// and s=0.5 (the shared seam, exclusive on (0,0)'s side and where (1,0)'s own interval starts) must find
// NOTHING here, because this fixture's hull is only 1 cell wide at y=0 -- there is no (1,0). Mutation: the
// `s >= lateral.Hi` admission test at FireControl.cs:~817 flipped to `s > lateral.Hi` would let s=0.5
// through as still touching (0,0) (0.5 is not > 0.5), which on a hull that DID have a second column would
// interleave the two columns' lanes at the seam -- reproduced directly here as "an empty lane's cell count
// is wrong", not by requiring the second column to exist.
[Fact]
public void LaneSeamIsHalfOpen ( )
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
{
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
var shape = new Shape ( 1 , 1 ) { [ new int2 ( 0 , 0 ) ] = true } ;
var hull = new HullData { Name = "Seam" , HullType = HullType . Ship , Shape = shape } ;
var b = float2 ( 0 , 1 ) ;
var buffer = new LaneCell [ shape . Coordinates . Length ] ;
var atLo = FireControl . Lane ( hull , b , - 0.5f , buffer ) ;
Assert . Equal ( 1 , atLo ) ;
Assert . Equal ( new int2 ( 0 , 0 ) , buffer [ 0 ] . Cell ) ;
var atHi = FireControl . Lane ( hull , b , 0.5f , buffer ) ;
Assert . Equal ( 0 , atHi ) ; // the seam itself: half-open on the Hi side, so this cell's own shadow excludes it
}
// Carried into 12.3's brief (12.2 Soul pass 3): AlongBearing/SlabAxis had no test of their own VALUES --
// every mutant there survived in 12.2 because only buffer[0]'s Cell was ever read (S1's own shadow
// prefilter, Extent, decided admission; AlongBearing's entry/exit were computed but unchecked). 12.3's own
// walk (FireControl.Apply) reads Entry for penetration and ordering, so a wrong entry/exit here is now a
2026-09-25 11:53:47 +02:00
// wrong damage cell, not dead arithmetic.
// F10 fix batch (Soul, 2026-09-25): the previous, single-bearing version of this test wrote out SlabAxis's
// OWN threshold (`1e-9f`) and clamp a second time, so a mutant that only moved that threshold (C7b, 1e-9 to
// .2) agreed with both copies identically and survived. This version never special-cases a near-zero
// bearing component at all: IEEE division by a signed zero already yields the correctly signed infinity, so
// an axis with no motion is unconstrained without a hand-written epsilon to disagree with SlabAxis's own.
// Checked at several bearings, including one within about 11 degrees of an axis -- inside SlabAxis's
// mutated .2f threshold but nowhere near the real 1e-9f one, so C7b now has to answer for a visibly wrong
// entry/exit instead of an unstressed one.
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
[Fact]
2026-09-25 11:53:47 +02:00
public void AlongBearingReturnsTheExactSlabIntersectionAtSeveralBearings ( )
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
{
var shape = SolidShape ( 5 , 5 ) ;
var hull = new HullData { Name = "Diagonal" , HullType = HullType . Ship , Shape = shape } ;
2026-09-25 11:53:47 +02:00
( double Entry , double Exit ) ExactSlabIndependent ( int2 c , double bx , double by , double ellx , double elly , double s )
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
{
2026-09-25 11:53:47 +02:00
var valX = s * ellx ; var valY = s * elly ;
var t1X = ( c . x - . 5 - valX ) / bx ; var t2X = ( c . x + . 5 - valX ) / bx ;
var t1Y = ( c . y - . 5 - valY ) / by ; var t2Y = ( c . y + . 5 - valY ) / by ;
var entry = Math . Max ( Math . Min ( t1X , t2X ) , Math . Min ( t1Y , t2Y ) ) ;
var exit = Math . Min ( Math . Max ( t1X , t2X ) , Math . Max ( t1Y , t2Y ) ) ;
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
return ( entry , exit ) ;
}
2026-09-25 11:53:47 +02:00
void Check ( float2 bRaw , float s )
{
var b = normalize ( bRaw ) ;
var ell = float2 ( - b . y , b . x ) ;
var h = ( Math . Abs ( ell . x ) + Math . Abs ( ell . y ) ) / 2.0 ; // Extent's own half-width, written out independently
var expectedCells = shape . Coordinates
. Select ( c = > ( Cell : c , Centre : ( double ) dot ( ( float2 ) c , ell ) ) )
. Where ( t = > s > = t . Centre - h & & s < t . Centre + h ) // Extent's own half-open shadow
. Select ( t = > t . Cell )
. OrderBy ( c = > dot ( ( float2 ) c , b ) )
. ThenBy ( c = > c . x ) . ThenBy ( c = > c . y )
. ToList ( ) ;
Assert . True ( expectedCells . Count > = 3 , $"fixture: b={b.x:R},{b.y:R} s={s} must cross several cells, got {expectedCells.Count}" ) ;
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
2026-09-25 11:53:47 +02:00
var buffer = new LaneCell [ shape . Coordinates . Length ] ;
var count = FireControl . Lane ( hull , b , s , buffer ) ;
var actualCells = Enumerable . Range ( 0 , count ) . Select ( i = > buffer [ i ] . Cell ) . ToList ( ) ;
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
2026-09-25 11:53:47 +02:00
Assert . Equal ( expectedCells , actualCells ) ; // same cells, same order -- entry ordering and contiguity agree
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
2026-09-25 11:53:47 +02:00
for ( var i = 0 ; i < count ; i + + )
{
var ( expectedEntry , expectedExit ) = ExactSlabIndependent ( buffer [ i ] . Cell , b . x , b . y , ell . x , ell . y , s ) ;
Assert . Equal ( expectedEntry , buffer [ i ] . Entry , 3 ) ;
Assert . Equal ( expectedExit , buffer [ i ] . Exit , 3 ) ;
}
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
}
2026-09-25 11:53:47 +02:00
Check ( float2 ( 2 , 1 ) , 0.13f ) ; // neither axis-aligned nor 45 degrees (the original fixture)
Check ( float2 ( 1 , . 15f ) , . 3f ) ; // ~8.5 degrees off the x-axis -- by sits inside C7b's mutated .2f threshold
Check ( float2 ( . 15f , 1 ) , - . 4f ) ; // the same, off the y-axis -- bx sits inside C7b's mutated .2f threshold
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
}
// Cut 12.3: replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own deleted 0.5-step march --
// that rule is gone with the function. The rule this now pins is the NEW thing FireControl.Apply's lane
// walk adds on top of Lane (Lane's own cell set/order/entry values are independently pinned elsewhere, by
2026-09-25 11:53:47 +02:00
// AlongBearingReturnsTheExactSlabIntersectionAtSeveralBearings and the LaneNeverEmptiesAtAnIntervalEdge sweep): the
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
// penetration cutoff. So this calls the real, already-verified FireControl.Lane for the walked cells and
// their entry parameters, then applies the cutoff independently -- kept only while within `penetration`
// cells of the first (impact) cell's own entry; the impact cell is always kept, matching the deleted
// `> .5f` threshold's intent at zero cost.
private static List < int2 > ExpectedLaneCells ( HullData hull , float2 bearing , float lateral , float penetration )
{
var buffer = new LaneCell [ hull . Shape . Coordinates . Length ] ;
var walked = FireControl . Lane ( hull , bearing , lateral , buffer ) ;
var result = new List < int2 > ( ) ;
if ( walked = = 0 ) return result ;
var impactEntry = buffer [ 0 ] . Entry ;
for ( var i = 0 ; i < walked ; i + + )
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
{
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
if ( i > 0 & & buffer [ i ] . Entry - impactEntry > = penetration ) break ;
result . Add ( buffer [ i ] . Cell ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
}
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
return result ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
}
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
// TurningAfterCommitChangesNothing (R4, closes L464/L465): after commit, turning the target and moving the
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
// shooter must not change where the damage actually lands -- Apply reads no live position or facing.
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
// F5 (Soul's fix batch, 2026-09-24): the original fixture fired with Penetration 0, so the penetration march
// never ran and never actually read the bearing Apply passed it -- a mutant that fed Apply a live
// (post-turn) bearing instead of the committed one survived (M2a) because nothing downstream of Cell ever
// consumed Bearing. Penetration 3 walks the lane several cells deep (Cut 12.3: FireControl.Apply's own lane
// walk, replacing the deleted Entity.ApplyHit march), so the full damaged-cell set is asserted against the
// committed Bearing/Lateral, not the live one.
// Cut 12.3 fix: this fixture's hull carries the flat 1000 armour every Build() call authors, and the Gun's
// own Damage is a fixed 5 -- fine for the old even-split model (every cell in the march took an
// undetectable-but-nonzero 5/N share), fatal for the new sequential one, where the very first (impact) cell
// alone would absorb the whole 5 and leave nothing for the rest of a several-cell-deep lane to show. Lowered
// to 0.05 per cell here so 5 damage plainly carries the full penetration-3 span.
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
[Fact]
public void TurningAfterCommitChangesNothing ( )
{
var settings = TestSettings ( ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
var e = Build ( settings , SolidShape ( 7 , 7 ) , precision : . 5f , velocity : 20 , penetration : 3 ) ; // 5s flight, commits at 4.5s
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
foreach ( var v in e . HullData . Shape . Coordinates ) { e . Target . Armor [ v . x , v . y ] = 0.05f ; e . Target . MaxArmor [ v . x , v . y ] = 0.05f ; }
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
ShotOutcome committed = null ;
using var c = e . Zone . ShotCommitted . Subscribe ( o = > committed = o ) ;
// The roll can still miss outright -- retry (a fresh shot each time; the previous miss already
// resolved and left the pending queue empty) until one hits.
for ( var attempt = 0 ; attempt < 30 & & ( committed = = null | | ! committed . Hit ) ; attempt + + )
{
committed = null ;
FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
e . Zone . Update ( 4.6f ) ; // past commit (4.5s), short of arrival (5s)
}
Assert . NotNull ( committed ) ;
Assert . True ( committed . Hit , "precondition: this fixture's shot must hit" ) ;
var expectedCell = NearestOccupiedCellAtLateral ( e . HullData , committed . Bearing , committed . Lateral ) ;
Assert . Equal ( expectedCell , committed . Cell ) ; // sanity: production's own Lane agrees with the independent recompute
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
var expectedFromCommitted = ExpectedLaneCells ( e . HullData , committed . Bearing , committed . Lateral , 3f ) . ToHashSet ( ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
var before = ( float [ , ] ) e . Target . Armor . Clone ( ) ;
// After commit, before arrival: the target turns 90 degrees and the shooter moves.
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
var postTurnFacing = float2 ( 1 , 0 ) ;
e . Target . Direction = postTurnFacing ;
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
e . Shooter . Position + = float3 ( 500 , 0 , 500 ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
// Fixture precondition: the (wrong) march a live-bearing bug would produce must actually differ from
// the committed one, or this test cannot tell the two apart. "Live" here means exactly what a mutant
// recomputing the bearing fresh at Apply time would read: the current TravelDirection (now stale,
// post-move) folded through the target's NEW facing -- both public FireControl functions, not a
// hand-rolled copy of the frame math.
var liveTravelDirection = FireControl . TravelDirection ( e . Weapon , e . Shooter , e . Target ) ;
var wrongBearing = normalize ( e . Target . ToSchematic ( liveTravelDirection ) ) ;
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
var expectedFromLive = ExpectedLaneCells ( e . HullData , wrongBearing , committed . Lateral , 3f ) . ToHashSet ( ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
Assert . NotEqual ( expectedFromCommitted , expectedFromLive ) ;
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
e . Zone . Update ( 2f ) ; // past arrival (5s)
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
foreach ( var cell in expectedFromCommitted )
Assert . True ( e . Target . Armor [ cell . x , cell . y ] < before [ cell . x , cell . y ] ,
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
$"cell {cell} is on the committed lane and must have taken damage, regardless of the post-commit turn" ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
foreach ( var cell in expectedFromLive )
if ( ! expectedFromCommitted . Contains ( cell ) )
Assert . Equal ( before [ cell . x , cell . y ] , e . Target . Armor [ cell . x , cell . y ] ) ;
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
}
// PlacementAndProbabilityShareOneSilhouette replaces 6d's PlacementAndProbabilityShareOneKernel: pins that
// CommitProbability's number and Commit's own placement are read from the exact same Silhouette. A
// diagonal bearing (|fx| ~ |fy|) makes individual cells' raw shadow intervals overlap heavily before
// merging, so a skipped merge (double-counted mass) or a swapped ell/b would show up here.
[Fact]
public void PlacementAndProbabilityShareOneSilhouette ( )
{
var settings = TestSettings ( ) ;
const float precision = . 45f ;
var hullShape = SolidShape ( 7 , 9 ) ;
// Aim off the hull's own centre of mass (near a corner) so the Gaussian's mass is not symmetric
// inside the shadow -- a perturbed sigma or a mis-signed ell moves the analytic and reported numbers
// apart even though both would otherwise report something close to 1.
var aim = new int2 ( 1 , 1 ) ;
var e = Build ( settings , hullShape , precision , spread : 0 , targetFacing : normalize ( float2 ( 1f , 1f ) ) ,
markerCells : new [ ] { aim } ) ;
Assert . True ( e . Shooter . TrySelectTargetItem ( e . Markers [ 0 ] ) ) ;
var shotId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
var pending = e . Zone . PendingShots . Single ( s = > s . ShotId = = shotId ) ;
Assert . Same ( e . Markers [ 0 ] , pending . Aimed ) ;
var bearing = normalize ( e . Target . ToSchematic ( pending . TravelDirection ) ) ;
var ell = float2 ( - bearing . y , bearing . x ) ;
var sigma = Math . Max ( 1f / precision , . 5f ) ;
var aimPoint = ( float2 ) aim ; // ResolveAimPoint on a single-cell marker: that cell itself
var a = dot ( aimPoint , ell ) ;
// Independent shadow: raw per-cell intervals from the hull's own public Shape.Coordinates, sorted and
// merged by this test, not by calling FireControl.Silhouette.
var raw = e . HullData . Shape . Coordinates
. Select ( c = > dot ( ( float2 ) c , ell ) )
. Select ( centre = > ( Lo : centre - ( Math . Abs ( ell . x ) + Math . Abs ( ell . y ) ) / 2f , Hi : centre + ( Math . Abs ( ell . x ) + Math . Abs ( ell . y ) ) / 2f ) )
. OrderBy ( iv = > iv . Lo )
. ToList ( ) ;
var merged = new List < ( float Lo , float Hi ) > ( ) ;
foreach ( var iv in raw )
{
if ( merged . Count > 0 & & iv . Lo < = merged [ merged . Count - 1 ] . Hi )
merged [ merged . Count - 1 ] = ( merged [ merged . Count - 1 ] . Lo , Math . Max ( merged [ merged . Count - 1 ] . Hi , iv . Hi ) ) ;
else
merged . Add ( iv ) ;
}
// Numeric integral of the Gaussian pdf over the merged union -- independent of erf, and of any private
// FireControl helper.
double Mass ( ( float Lo , float Hi ) iv )
{
const int steps = 4000 ;
var width = ( iv . Hi - iv . Lo ) / steps ;
if ( width < = 0 ) return 0 ;
double sum = 0 ;
for ( var i = 0 ; i < = steps ; i + + )
{
var x = iv . Lo + i * width ;
var z = ( x - a ) / sigma ;
var pdf = Math . Exp ( - z * z / 2.0 ) / ( sigma * Math . Sqrt ( 2.0 * Math . PI ) ) ;
sum + = pdf * ( i = = 0 | | i = = steps ? . 5 : 1.0 ) ;
}
return sum * width ;
}
var expectedPOnHull = merged . Sum ( Mass ) ;
Assert . True ( expectedPOnHull > . 05 & & expectedPOnHull < . 95 , $"fixture: pOnHull ({expectedPOnHull}) must be non-degenerate to exercise the merge, got a saturated or collapsed value" ) ;
var commitP = FireControl . CommitProbability ( pending , e . Zone . Time , out var sil ) ;
// Fixture isolation: PFire = Accuracy(1) x PSensor(1, full reveal) = 1; pDeviation = 1 (target hasn't
// moved since FireTargetPosition was frozen); PSpread = 1 (Spread 0's own early-out). So the reported
// commit price collapses to exactly pOnHull, the number this test computed independently.
Assert . Equal ( ( float ) expectedPOnHull , commitP , 3 ) ;
Assert . Equal ( ( float ) expectedPOnHull , sil . POnHull , 3 ) ;
// Empirical: the hit rate and the per-interval Lateral shares both match the analytic union.
const int shots = 4000 ;
var outcomes = FireMany ( e , shots ) ;
var hits = outcomes . Where ( o = > o . Hit ) . ToList ( ) ;
Assert . True ( hits . Count > shots * expectedPOnHull * . 5 , $"expected roughly {shots * expectedPOnHull:0} hits, got {hits.Count}" ) ;
Assert . True ( Math . Abs ( hits . Count / ( double ) shots - expectedPOnHull ) < . 05 , $"empirical hit rate {hits.Count / (double) shots:F3} does not track analytic pOnHull {expectedPOnHull:F3}" ) ;
foreach ( var ( lo , hi ) in merged )
{
var expectedShare = Mass ( ( lo , hi ) ) / expectedPOnHull ;
if ( expectedShare < . 05 ) continue ; // too little mass to measure reliably over 4000 shots
var empiricalShare = hits . Count ( o = > o . Lateral > = lo - 1e-3f & & o . Lateral < hi + 1e-3f ) / ( double ) hits . Count ;
Assert . True ( Math . Abs ( expectedShare - empiricalShare ) < . 1 , $"interval [{lo:F2},{hi:F2}): expected share {expectedShare:F3}, empirical {empiricalShare:F3}" ) ;
}
}
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
// F3 (Soul's fix batch, 2026-09-24): every 12.2 fixture, and every shipped hull (Soul's own 360-degree
// sweep), projects to exactly ONE merged interval at its test bearing -- LateralDraw's walk past the
// first interval (the `cumulative += mass` loop continuation) and a Silhouette built with the wrong sigma
// (M12) were both unreachable. This hull is two 2-wide prongs (x in {0,1} and {5,6}, all rows) separated
// by a real gap (x in 2..4, unoccupied) -- a dead-ahead bearing (ell=(-1,0)) gives two disjoint shadow
// intervals, symmetric around the hull's own centre of mass (3,2), which sits in the gap.
[Fact]
public void TwoProngHullSplitsTheScatterAcrossBothIntervals ( )
{
var shape = new Shape ( 7 , 5 ) ;
foreach ( var x in new [ ] { 0 , 1 , 5 , 6 } )
for ( var y = 0 ; y < 5 ; y + + )
shape [ new int2 ( x , y ) ] = true ;
const float sigma = 1.5f ;
const float precision = 1f / sigma ;
var e = Build ( TestSettings ( ) , shape , precision ) ; // unaimed: centre of mass (3,2)
// The bearing a shot would freeze, read directly (not through an actual Fire call, which would leave
// a stray shot in the queue for FireMany to double-count).
var travelDirection = FireControl . TravelDirection ( e . Weapon , e . Shooter , e . Target ) ;
var bearing = normalize ( e . Target . ToSchematic ( travelDirection ) ) ;
var ell = float2 ( - bearing . y , bearing . x ) ;
var a = dot ( shape . CenterOfMass , ell ) ;
// Independent shadow (raw per-cell intervals, sorted and merged), the same convention
// PlacementAndProbabilityShareOneSilhouette uses.
var raw = shape . Coordinates
. Select ( c = > dot ( ( float2 ) c , ell ) )
. Select ( centre = > ( Lo : centre - ( Math . Abs ( ell . x ) + Math . Abs ( ell . y ) ) / 2f , Hi : centre + ( Math . Abs ( ell . x ) + Math . Abs ( ell . y ) ) / 2f ) )
. OrderBy ( iv = > iv . Lo )
. ToList ( ) ;
var merged = new List < ( float Lo , float Hi ) > ( ) ;
foreach ( var iv in raw )
{
if ( merged . Count > 0 & & iv . Lo < = merged [ merged . Count - 1 ] . Hi )
merged [ merged . Count - 1 ] = ( merged [ merged . Count - 1 ] . Lo , Math . Max ( merged [ merged . Count - 1 ] . Hi , iv . Hi ) ) ;
else
merged . Add ( iv ) ;
}
Assert . Equal ( 2 , merged . Count ) ; // fixture precondition: a real two-interval shadow
double Mass ( ( float Lo , float Hi ) iv )
{
const int steps = 4000 ;
var width = ( iv . Hi - iv . Lo ) / steps ;
double sum = 0 ;
for ( var i = 0 ; i < = steps ; i + + )
{
var x = iv . Lo + i * width ;
var z = ( x - a ) / sigma ;
var pdf = Math . Exp ( - z * z / 2.0 ) / ( sigma * Math . Sqrt ( 2.0 * Math . PI ) ) ;
sum + = pdf * ( i = = 0 | | i = = steps ? . 5 : 1.0 ) ;
}
return sum * width ;
}
var mass0 = Mass ( merged [ 0 ] ) ;
var mass1 = Mass ( merged [ 1 ] ) ;
var totalMass = mass0 + mass1 ;
Assert . True ( totalMass > . 05 & & totalMass < . 95 , $"fixture: total pOnHull ({totalMass}) must be non-degenerate" ) ;
Assert . True ( Math . Abs ( mass0 - mass1 ) / totalMass < . 05 , "fixture: the two prongs must be near-symmetric for a clean 50/50 check" ) ;
var outcomes = FireMany ( e , 8000 ) ;
var hits = outcomes . Where ( o = > o . Hit ) . ToList ( ) ;
Assert . True ( hits . Count > 800 , $"expected a healthy number of hits, got {hits.Count}" ) ;
var inProngA = hits . Count ( o = > o . Cell . x < = 1 ) ;
var inProngB = hits . Count ( o = > o . Cell . x > = 5 ) ;
Assert . Equal ( hits . Count , inProngA + inProngB ) ; // every hit lands in one prong or the other, never the gap
var empiricalShareA = inProngA / ( double ) hits . Count ;
var expectedShareA = mass0 / totalMass ;
Assert . True ( Math . Abs ( empiricalShareA - expectedShareA ) < . 05 ,
$"prong A's empirical share {empiricalShareA:F3} does not match its analytic share {expectedShareA:F3}" ) ;
2026-09-25 03:11:27 +02:00
// S2 fix batch (Hands, 2026-09-25): within-prong SHAPE, pinned by ratio, not just direction. Prong A is
// x in {0,1}, projected lateral centres 0 and -1 respectively (ell=(-1,0)), a=-3. The old
// "atNearColumn > atFarColumn" check survived M12/M12b (an entire second silhouette built at sigma
// floor .5 or at sigma*sqrt(2)) and the sigma/sqrt(2) Stryker mutants at :~753/:~750, because every one
// of those wrong sigmas still puts more mass on the nearer column -- direction alone can't tell a
// sigma of 1.5 from a sigma of 1.5*sqrt(2). The RATIO can: Soul's own figures are ~2.9 at the true
// sigma against ~1.73 at sigma*sqrt(2), well separated at 8000 shots. The near and far columns' own raw
// (unmerged) intervals are exactly the two ends of `raw` for prong A, reused here rather than
// recomputed.
var nearInterval = raw . First ( iv = > Math . Abs ( iv . Lo - ( - 1.5f ) ) < 1e-3f ) ; // x=1: [-1.5,-0.5)
var farInterval = raw . First ( iv = > Math . Abs ( iv . Lo - ( - 0.5f ) ) < 1e-3f ) ; // x=0: [-0.5,0.5)
var nearMass = Mass ( nearInterval ) ;
var farMass = Mass ( farInterval ) ;
var analyticRatio = nearMass / farMass ;
Assert . True ( analyticRatio > 2.3 , $"fixture: analytic near/far ratio ({analyticRatio:F3}) must be well clear of the wrong-sigma ratio (~1.73) to actually distinguish the two" ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
var atNearColumn = hits . Count ( o = > o . Cell . x = = 1 ) ;
var atFarColumn = hits . Count ( o = > o . Cell . x = = 0 ) ;
2026-09-25 03:11:27 +02:00
Assert . True ( atNearColumn > 0 & & atFarColumn > 0 , $"expected hits on both columns, got near={atNearColumn} far={atFarColumn}" ) ;
var empiricalRatio = atNearColumn / ( double ) atFarColumn ;
Assert . True ( Math . Abs ( empiricalRatio - analyticRatio ) < . 6 ,
$"empirical near/far ratio {empiricalRatio:F3} ({atNearColumn}/{atFarColumn}) does not track the analytic ratio {analyticRatio:F3} -- a sigma perturbed by sqrt(2) would give ~1.73 instead" ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
}
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
// HitsLandOnTheFacingEdge supersedes EveryHitLandsOnMetal: on a concave (holed) hull, every hit's Cell is
// occupied, AND nothing occupied precedes it in its own lane -- the test recomputes the lane from the
// outcome's own frozen Bearing/Lateral, independently of FireControl.Lane, and checks production picked
// the nearest cell, not merely an occupied one.
[Fact]
public void HitsLandOnTheFacingEdge ( )
{
var shape = new Shape ( 7 , 7 ) ;
foreach ( var cell in shape . AllCoordinates ) shape [ cell ] = true ;
shape [ new int2 ( 3 , 3 ) ] = false ; // the hole
var aim = new int2 ( 1 , 1 ) ;
var e = Build ( TestSettings ( ) , shape , precision : . 3f , targetFacing : normalize ( float2 ( . 8f , . 6f ) ) , markerCells : new [ ] { aim } ) ;
Assert . True ( e . Shooter . TrySelectTargetItem ( e . Markers [ 0 ] ) ) ;
var outcomes = FireMany ( e , 1500 ) ;
var hits = outcomes . Where ( o = > o . Hit ) . ToList ( ) ;
Assert . True ( hits . Count > 100 , $"expected a healthy number of hits to check, got {hits.Count}" ) ;
foreach ( var outcome in hits )
{
Assert . True ( shape [ outcome . Cell ] , $"hit landed on {outcome.Cell}, not part of the hull's own schematic" ) ;
var expected = NearestOccupiedCellAtLateral ( e . HullData , outcome . Bearing , outcome . Lateral ) ;
Assert . Equal ( expected , outcome . Cell ) ;
}
}
// AimingAtAnItemCentresTheScatterOnItsLane closes L585/L587 and replaces AimingAtTheSternHitsTheStern,
// whose fixture fired from the target's own stern and would pass for the wrong reason (any placement rule
// that merely biases toward whatever is nearest the shooter, rather than toward the aim point specifically,
// would also pass a stern-fired, stern-aimed shot). From broadside, hits aimed at the stern land mostly in
// the stern half and hits aimed at the bow mostly in the bow half; from dead astern, hits aimed at the bow
// land on the stern edge (the near edge, since the aim point pulls the group toward the bow but every cell
// between the stern edge and the bow marker is on the lane first).
[Fact]
public void AimingAtAnItemCentresTheScatterOnItsLane ( )
{
var settings = TestSettings ( ) ;
var hullShape = SolidShape ( 3 , 11 ) ;
var bow = new int2 ( 1 , 9 ) ; // y=9: firmly in the bow half (y>=5.5)
var stern = new int2 ( 1 , 1 ) ; // y=1: firmly in the stern half
double SternFraction ( float2 facing , int2 aimCell )
{
var e = Build ( settings , hullShape , precision : . 6f , targetFacing : facing , markerCells : new [ ] { aimCell } ) ;
var aimed = e . Markers [ 0 ] ;
Assert . True ( e . Shooter . TrySelectTargetItem ( aimed ) ) ;
Assert . Same ( aimed , e . Shooter . ResolvedTargetItem ) ;
var outcomes = FireMany ( e , 600 ) ;
var hits = outcomes . Where ( o = > o . Hit ) . ToList ( ) ;
Assert . True ( hits . Count > 100 , $"expected a healthy number of hits, got {hits.Count}" ) ;
return hits . Count ( o = > o . Cell . y < 5.5f ) / ( double ) hits . Count ;
}
// Broadside: forward perpendicular to the shot's travel direction (world ~+z), so both bow and stern
// are roughly equidistant from the shooter and the aim point, not the entry side, drives the scatter.
var sternAimedBroadside = SternFraction ( float2 ( 1 , 0 ) , stern ) ;
var bowAimedBroadside = SternFraction ( float2 ( 1 , 0 ) , bow ) ;
Assert . True ( sternAimedBroadside > . 7 , $"broadside, aimed at the stern: expected mostly stern hits, got sternFraction={sternAimedBroadside}" ) ;
Assert . True ( bowAimedBroadside < . 3 , $"broadside, aimed at the bow: expected mostly bow hits, got sternFraction={bowAimedBroadside}" ) ;
// Dead astern: forward parallel to the shot's travel direction (bow points away from the shooter), so
// the shot enters through the stern regardless of aim -- aiming at the bow still lands on the near
// (stern) edge, because the stern cells are on the lane first.
var bowAimedAstern = SternFraction ( float2 ( 0 , 1 ) , bow ) ;
Assert . True ( bowAimedAstern > . 7 , $"dead astern, aimed at the bow: expected the near (stern) edge to still take the hits, got sternFraction={bowAimedAstern}" ) ;
}
// TheHudEstimateIsTheCommitPrice: for an in-flight shot, the value the HUD's own estimate (through
// FireControl.CommitProbability, the function ActionGameManager's pending-shot line now calls) and the p
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
// Commit itself rolls against are the same number at the commit tick.
// F4 (Soul's fix batch, 2026-09-24): the original test called CommitProbability twice and compared the
// two calls to each other -- a self-consistency check that never observed what Commit's own roll actually
// uses. Survivors: Commit applying PSpread twice (M15), and Commit rolling against PFire x pOnHull alone,
// dropping deviation and PSpread entirely (M16) -- both invisible to a test that never rolls anything.
// This version fires many shots against one fixed, non-trivial configuration (nonzero weapon Spread, and
// a target jinked off its fire-time projection so pDeviation is strictly less than 1) and checks the
// EMPIRICAL hit fraction over the real Commit path against the HUD's own forecast, with a tolerance sized
// from the sample (3 sigma of a Bernoulli(p, n)).
2026-09-25 03:17:38 +02:00
// S7 fix batch (Hands, 2026-09-25): Tracking is now authored through Build's own `tracking` parameter (one
// authoring path for every frozen shooter stat, matching Precision/Accuracy) instead of patching
// PendingShot.Tracking after every Fire -- the operator's own rule (2026-09-22) is that a fixture should
// author its state up front, not reach into a frozen field post hoc. An authored Tracking of 6 against this
// 5x7 hull and jink gives pDeviation ~= .33, strictly between 0 and 1, with no patching.
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
[Fact]
public void TheHudEstimateIsTheCommitPrice ( )
{
2026-09-25 03:17:38 +02:00
var e = Build ( TestSettings ( ) , SolidShape ( 5 , 7 ) , precision : . 5f , velocity : 40 , spread : 6f , tracking : 6f ) ; // 2.5s flight, commits at 2.0s
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
var origin = e . Target . Position ;
const int shots = 800 ;
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
var pendingSnapshots = new List < PendingShot > ( ) ;
for ( var i = 0 ; i < shots ; i + + )
{
e . Target . Position = origin ; // reset before each Fire so every shot freezes the same FireTargetPosition
var shotId = FireControl . Fire ( e . Weapon , e . WeaponItem , e . Shooter ) ;
e . Zone . Update ( . 001f ) ;
var index = e . Zone . PendingShots . FindIndex ( s = > s . ShotId = = shotId ) ;
2026-09-25 03:17:38 +02:00
pendingSnapshots . Add ( e . Zone . PendingShots [ index ] ) ;
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
}
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
// One jink, after every shot is queued, before any of them commit -- pDeviation < 1 for all of them,
// uniformly (FireTargetVelocity is 0, so DeviationProbability's projection never drifts with elapsed
// time; every shot sees the same deviation regardless of the small firing-order time skew above).
e . Target . Position = origin + float3 ( 4 , 0 , 0 ) ;
var hits = 0 ;
var commits = 0 ;
using var c = e . Zone . ShotCommitted . Subscribe ( o = > { commits + + ; if ( o . Hit ) hits + + ; } ) ;
e . Zone . Update ( 2.1f ) ; // past every shot's 2.0s commit, short of the 2.5s arrival
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
Cut 12.2 Soul fix batch: half-open clamp, split assembly, real rolls
F1 (correctness): LateralDraw clamped the drawn lateral to the closed
[Lo, Hi], not the spec's half-open [Lo, Hi). Soul enumerated all 2^24
NextFloat outputs against TurningArmourIntoTheShotTakesItOnTheArmour's own
geometry and found 3 that draw exactly Hi, which Lane's own half-open filter
then admits no cell for -- a hit that passed its roll landing nowhere (R3).
Fixed the clamp; Lane's silent int2.zero fallback on an empty lane is now a
thrown InvalidOperationException, since an empty lane is provably impossible
after the fix. Committed Soul's edge probe as
LateralDrawIsHalfOpenAtTheShadowsTopEdge.
F6/F8: HitProbability, Inspect and Fire each assembled bearing -> Silhouette
-> PSpread (or recomputed range) in their own copy; a shooter-frame bearing
bug in HitProbability's copy survived while the identical bug in Inspect's
copy was caught. Collapsed into one Forecast function and one PFire(out range)
overload; both callers, and Fire, read the one computation.
F2: FireControlCut12Tests' zone names carried a Guid, breaking every other
cut's fixed-seed convention (FireControlCut6Tests.cs:62-63) and reseeding the
die every run -- the actual source of the flake in
TurningArmourIntoTheShotTakesItOnTheArmour. Fixed to a fixed zone name.
F3/F4/F5/F7: added a two-prong hull fixture (LateralDraw's walk past the first
interval, and a wrong-sigma placement, were unreachable when every fixture
projects to one interval), a real-roll empirical test for CommitProbability
(the old one called the pure function twice and compared it to itself),
enough Penetration in TurningAfterCommitChangesNothing for ApplyHit to
actually read the bearing, and a 2x2 item fixture for ResolveAimPoint's
average (every "Marker" elsewhere is 1x1, so sum/length and sum*length agree).
F9: Lane's own comment claimed no allocation while its sort used
Comparer<T>.Create over a closure; LaneCell now carries its own precomputed
projection and Lane sorts with a stateless singleton comparer. Corrected an
overclaiming comment in FireAuthorityTests (SigmaFloor makes the aimed-lane
draw ~68% per shot, not deterministic; the fixture's fixed zone name is what
actually makes it reproduce).
Cost: Silhouette takes an optional caller-rented Interval[] buffer; the
forecast path (HitProbability/Inspect, via Forecast) rents from
ArrayPool<Interval>.Shared and returns it before returning, since neither
reads sil.Intervals after the call. Commit/CommitProbability still get an
owned array, since LateralDraw needs it alive past the call. Measured on
LonginusX (66 cells, Release): unaimed 672 B -> 184 B/call (1512-1637ns before,
1482-1560ns after); aimed 1160 B -> 672 B/call (2123-2140ns before,
2094-2162ns after) -- the remaining ~488B/call gap between aimed and unaimed
is ResolveAimPoint's own CellsOf allocation (a List<int2>+ToArray per call),
not in this batch's scope.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 23:08:19 +02:00
Assert . Equal ( shots , commits ) ;
var expected = FireControl . CommitProbability ( pendingSnapshots [ 0 ] , e . Zone . Time , out var sil ) ;
Assert . True ( sil . POnHull > . 02f & & sil . POnHull < . 98f , $"fixture: pOnHull ({sil.POnHull}) must be non-degenerate" ) ;
var pDeviation = FireControl . DeviationProbability ( pendingSnapshots [ 0 ] , e . Zone . Time , out var deviation ) ;
Assert . True ( deviation > 0f , "fixture: the jink must actually move the target off its projection" ) ;
Assert . True ( pDeviation > . 02f & & pDeviation < . 98f , $"fixture: pDeviation ({pDeviation}) must be strictly between 0 and 1" ) ;
Assert . True ( expected > . 02f & & expected < . 98f , $"fixture: the commit price ({expected}) must be strictly between 0 and 1" ) ;
var empirical = hits / ( double ) shots ;
// 3 standard deviations of a Bernoulli(expected, shots) draw, so this fails on the real bug (a
// dropped or duplicated factor) far more often than on sampling noise.
var tolerance = 3.0 * Math . Sqrt ( expected * ( 1 - expected ) / shots ) ;
Assert . True ( Math . Abs ( empirical - expected ) < tolerance ,
$"empirical hit rate {empirical:F4} over {shots} shots is not within {tolerance:F4} of the HUD's forecast {expected:F4}" ) ;
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
}
// TheSigmaFloorHoldsAtHalfACell: pOnHull at Precision 1000 equals pOnHull at Precision 2 on a one-cell
// lane. Cut 12.2's exact integral no longer collapses on its own (POnHullNeverCollapsesAboveThePrecisionCliff
// no longer catches a deleted floor), so this is the test that kills "delete SigmaFloor" -- without the
// floor, sigma at Precision 1000 would be 0.001, and the Gaussian would collapse almost entirely inside a
// single cell's own width rather than sharing the whole one-cell lane the way Precision 2 (sigma .5,
// already at the floor) does.
[Fact]
public void TheSigmaFloorHoldsAtHalfACell ( )
{
// A 1-wide, 9-tall hull: every occupied cell's shadow along a dead-ahead bearing collapses onto the
// same single-cell-wide lane (ell has one axis zero, so h and the per-cell interval width degenerate
// to the schematic's own cell width) -- "a one-cell lane" per the spec's own name for this test. A
// 1-wide hull has no interior cell at all (Shrink needs a full ring), so this fires unaimed -- the
// hull's own centre of mass, (0,4), is already the middle of the lane.
var shape = SolidShape ( 1 , 9 ) ;
float POnHullAt ( float precision )
{
var e = Build ( TestSettings ( ) , shape , precision ) ;
return FireControl . Inspect ( e . Weapon , e . Shooter , e . Target ) . POnHull ;
}
var at2 = POnHullAt ( 2f ) ;
var at1000 = POnHullAt ( 1000f ) ;
Assert . Equal ( at2 , at1000 , 4 ) ;
}
// ShieldIsOmnidirectional: an active shield absorbs the same shot identically from bow, stern and beam --
// the shield gate in Commit reads only DamageType/Damage, never the bearing this cut introduces.
[Fact]
public void ShieldIsOmnidirectional ( )
{
var settings = TestSettings ( ) ;
bool AbsorbsFrom ( float2 facing )
{
var e = Build ( settings , SolidShape ( 5 , 5 ) , precision : 1f , targetFacing : facing , equipShield : true , shieldCapacity : 1000 ) ;
var before = e . Target . Hull . Durability ;
var outcomes = FireMany ( e , 50 ) ;
var hits = outcomes . Where ( o = > o . Hit ) . ToList ( ) ;
Assert . True ( hits . Count > 5 , $"expected a healthy number of hits, got {hits.Count}" ) ;
Assert . True ( hits . All ( o = > o . Shielded ) , "every hit must be absorbed by the shield regardless of facing" ) ;
Assert . Equal ( before , e . Target . Hull . Durability ) ;
return true ;
}
Assert . True ( AbsorbsFrom ( float2 ( 0 , 1 ) ) ) ; // bow toward the shot
Assert . True ( AbsorbsFrom ( float2 ( 0 , - 1 ) ) ) ; // stern toward the shot
Assert . True ( AbsorbsFrom ( float2 ( 1 , 0 ) ) ) ; // beam
}
Cut 12.2 Soul fix batch 2, S3: measure allocations, correct an overclaiming comment
Measured (Release, 7x9 solid hull -- the LonginusX shipped-hull fixture the
brief named is not available to this test file):
- Lane: 0 B / 1000 calls (was: 64 B/call from Array.Sort's comparer wrapper;
now confirmed 0 after the earlier IComparable<T> change in this batch).
- Silhouette (caller-supplied buffer): 0 B / 1000 calls.
- HitProbability, gated in (a real, non-gated-out call): ~120 B/call. This is
NOT from Lane, Silhouette or the comparer -- it comes from Accuracy/
Resolution's own GetBehavior<T>, which walks Entity.Equipment and each
item's Behaviors (both ReactiveCollection<T>); that type's enumerator is
heap-allocated per foreach. This predates Cut 12.2 and is out of this cut's
scope to fix.
- Inspect, gated in: ~280 B/call (same cause, more GetBehavior<T> calls).
The Forecast comment claiming "the forecast path (HitProbability, Inspect) no
longer allocates on a gated-in call" was not true and is corrected to say what
was actually measured: Silhouette itself is allocation-free given a pooled
buffer, and the residual cost belongs to Entity's own collection choice.
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer asserts the true zero-alloc
claims exactly and records (with a loose regression bound, not a false zero)
the unrelated Entity-level cost on the full HitProbability path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:30:30 +02:00
// S3 fix batch (Hands, 2026-09-25): Lane's own comment ("never allocates on its own") and Silhouette's
// pooled path are pinned directly, instead of trusting the comment. Measured (Release, LonginusX-shaped
// fixture unavailable to this test file -- a 7x9 solid hull is used instead): Lane and Silhouette (given a
// caller-supplied buffer) allocate exactly 0 bytes over 1000 calls, confirming the S3 change itself
// (Array.Sort(T[], int, int) resolving Interval/LaneCell's own IComparable<T> at JIT time, not the
// (T[], int, int, IComparer<T>) overload that wrapped even a cached singleton comparer in a fresh delegate
// per call). A full gated-in HitProbability call is NOT zero-allocation (measured ~120 B/call, Release) --
Cut 12.2 carried items: pin Lane's seam contract and AlongBearing's values
Items carried into 12.3's brief from 12.2's third Soul pass, all on code
12.3's own lane walk builds on:
- LaneSeamIsHalfOpen pins the s >= Hi admission test (FireControl.cs) on
the smallest fixture where the seam matters -- the s > Hi mutant that
survived 12.2 would have interleaved two columns at a shared seam once
12.3's walk started reading more than buffer[0].
- AlongBearingReturnsTheExactSlabIntersection gives AlongBearing/SlabAxis
their first test of actual VALUES (every mutant there survived in 12.2,
equivalent while only buffer[0].Cell was read) -- a diagonal fixture
checks Lane's own Entry/Exit against an independent slab solve, cell for
cell, in order.
- Corrected the allocation comments on Forecast and
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer: the ~120 B/call on a
gated-in HitProbability is three GetBehavior<TargetingSystem> calls (not
EquippedItem.Behaviors, which is a plain array), plus CellsOf's ~944 B
when an item is aimed at; noted that the loose <500_000 bound doesn't
catch a reintroduced comparer wrapper but the buffered zero-allocation
asserts do.
- ZeroProbabilityShotCommitsAMissWithoutBuildingASilhouette is now
behavioural: PFire 0 comes from closing VisibleEntities (the real gate),
and "no Silhouette was built" is checked by asserting zero allocation
rather than reading CommitProbability's own struct default.
- Dropped the unused System.Reflection import.
Also replaces ExpectedMarchCells, which reproduced Entity.ApplyHit's own
deleted 0.5-step march, with ExpectedLaneCells, built on the
already-verified FireControl.Lane plus an independent penetration cutoff --
used by TurningAfterCommitChangesNothing, whose fixture also gets real
per-cell armour (the flat 1000 armour every Build() authors would let the
new sequential Absorb model's first cell alone swallow the whole 5-damage
shot, leaving nothing to show on the rest of a several-cell lane).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 05:56:30 +02:00
// 12.3 fix batch correction: that cost is exactly three GetBehavior<TargetingSystem> calls (Accuracy,
// Resolution, Precision, ~40 B each), each walking Entity.Equipment (a ReactiveCollection<T>, heap-allocated
// enumerator per foreach) -- NOT `Behaviors` (a plain array, the earlier wording here was wrong; see the
// corrected comment on Forecast). It predates this cut and is out of its scope. The loose `< 500_000` bound
// below is a regression trip-wire, not a proof: a reintroduced per-call IComparer<T> wrapper (~184 B/call)
// would pass it easily. The two zero-allocation asserts just above are what actually catch that regression
// -- Lane and Silhouette are exactly where such a wrapper would reappear -- so the bound here only needs to
// catch something far larger, such as a reintroduced unpooled Interval[]/LaneCell[] on this path.
Cut 12.2 Soul fix batch 2, S3: measure allocations, correct an overclaiming comment
Measured (Release, 7x9 solid hull -- the LonginusX shipped-hull fixture the
brief named is not available to this test file):
- Lane: 0 B / 1000 calls (was: 64 B/call from Array.Sort's comparer wrapper;
now confirmed 0 after the earlier IComparable<T> change in this batch).
- Silhouette (caller-supplied buffer): 0 B / 1000 calls.
- HitProbability, gated in (a real, non-gated-out call): ~120 B/call. This is
NOT from Lane, Silhouette or the comparer -- it comes from Accuracy/
Resolution's own GetBehavior<T>, which walks Entity.Equipment and each
item's Behaviors (both ReactiveCollection<T>); that type's enumerator is
heap-allocated per foreach. This predates Cut 12.2 and is out of this cut's
scope to fix.
- Inspect, gated in: ~280 B/call (same cause, more GetBehavior<T> calls).
The Forecast comment claiming "the forecast path (HitProbability, Inspect) no
longer allocates on a gated-in call" was not true and is corrected to say what
was actually measured: Silhouette itself is allocation-free given a pooled
buffer, and the residual cost belongs to Entity's own collection choice.
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer asserts the true zero-alloc
claims exactly and records (with a loose regression bound, not a false zero)
the unrelated Entity-level cost on the full HitProbability path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:30:30 +02:00
[Fact]
public void LaneAndSilhouetteAllocateNothingGivenAPooledBuffer ( )
{
var e = Build ( TestSettings ( ) , SolidShape ( 7 , 9 ) , precision : . 5f ) ;
var hull = e . HullData ;
var travelDirection = FireControl . TravelDirection ( e . Weapon , e . Shooter , e . Target ) ;
var bearing = normalize ( e . Target . ToSchematic ( travelDirection ) ) ;
var sil = FireControl . Silhouette ( e . Target , hull , null , bearing , . 5f ) ;
var laneBuffer = new LaneCell [ hull . Shape . Coordinates . Length ] ;
var intervalBuffer = new Interval [ hull . Shape . Coordinates . Length ] ;
for ( var i = 0 ; i < 100 ; i + + ) FireControl . Lane ( hull , bearing , sil . A , laneBuffer ) ; // JIT warm-up
for ( var i = 0 ; i < 100 ; i + + ) FireControl . Silhouette ( e . Target , hull , null , bearing , . 5f , intervalBuffer ) ;
2026-09-29 04:17:43 +02:00
// Steady state, not first-use: under a whole-suite run the runtime's background tier-up is delayed by the
// other tests' JIT activity, so a one-off allocation (measured: 1232 bytes over 1000 calls, in 1 of 12
// full-suite runs and 0 of 100 isolated runs) can land inside a single window. That is a cost paid once.
// A per-call allocation, the regression this test guards, lands in EVERY round, so the smallest round
// over a bounded number of rounds is the steady-state allocation.
long SmallestRoundAllocation ( Action round )
{
var smallest = long . MaxValue ;
for ( var r = 0 ; r < 20 & & smallest ! = 0 ; r + + )
{
var start = GC . GetAllocatedBytesForCurrentThread ( ) ;
round ( ) ;
smallest = Math . Min ( smallest , GC . GetAllocatedBytesForCurrentThread ( ) - start ) ;
}
return smallest ;
}
var laneAllocated = SmallestRoundAllocation ( ( ) = > { for ( var i = 0 ; i < 1000 ; i + + ) FireControl . Lane ( hull , bearing , sil . A , laneBuffer ) ; } ) ;
Assert . True ( laneAllocated = = 0 , $"1000 Lane calls allocated {laneAllocated} bytes in every round; \" never allocates \ " is not true" ) ;
Cut 12.2 Soul fix batch 2, S3: measure allocations, correct an overclaiming comment
Measured (Release, 7x9 solid hull -- the LonginusX shipped-hull fixture the
brief named is not available to this test file):
- Lane: 0 B / 1000 calls (was: 64 B/call from Array.Sort's comparer wrapper;
now confirmed 0 after the earlier IComparable<T> change in this batch).
- Silhouette (caller-supplied buffer): 0 B / 1000 calls.
- HitProbability, gated in (a real, non-gated-out call): ~120 B/call. This is
NOT from Lane, Silhouette or the comparer -- it comes from Accuracy/
Resolution's own GetBehavior<T>, which walks Entity.Equipment and each
item's Behaviors (both ReactiveCollection<T>); that type's enumerator is
heap-allocated per foreach. This predates Cut 12.2 and is out of this cut's
scope to fix.
- Inspect, gated in: ~280 B/call (same cause, more GetBehavior<T> calls).
The Forecast comment claiming "the forecast path (HitProbability, Inspect) no
longer allocates on a gated-in call" was not true and is corrected to say what
was actually measured: Silhouette itself is allocation-free given a pooled
buffer, and the residual cost belongs to Entity's own collection choice.
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer asserts the true zero-alloc
claims exactly and records (with a loose regression bound, not a false zero)
the unrelated Entity-level cost on the full HitProbability path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:30:30 +02:00
2026-09-29 04:17:43 +02:00
var silhouetteAllocated = SmallestRoundAllocation ( ( ) = > { for ( var i = 0 ; i < 1000 ; i + + ) FireControl . Silhouette ( e . Target , hull , null , bearing , . 5f , intervalBuffer ) ; } ) ;
Assert . True ( silhouetteAllocated = = 0 , $"1000 buffered Silhouette calls allocated {silhouetteAllocated} bytes in every round" ) ;
Cut 12.2 Soul fix batch 2, S3: measure allocations, correct an overclaiming comment
Measured (Release, 7x9 solid hull -- the LonginusX shipped-hull fixture the
brief named is not available to this test file):
- Lane: 0 B / 1000 calls (was: 64 B/call from Array.Sort's comparer wrapper;
now confirmed 0 after the earlier IComparable<T> change in this batch).
- Silhouette (caller-supplied buffer): 0 B / 1000 calls.
- HitProbability, gated in (a real, non-gated-out call): ~120 B/call. This is
NOT from Lane, Silhouette or the comparer -- it comes from Accuracy/
Resolution's own GetBehavior<T>, which walks Entity.Equipment and each
item's Behaviors (both ReactiveCollection<T>); that type's enumerator is
heap-allocated per foreach. This predates Cut 12.2 and is out of this cut's
scope to fix.
- Inspect, gated in: ~280 B/call (same cause, more GetBehavior<T> calls).
The Forecast comment claiming "the forecast path (HitProbability, Inspect) no
longer allocates on a gated-in call" was not true and is corrected to say what
was actually measured: Silhouette itself is allocation-free given a pooled
buffer, and the residual cost belongs to Entity's own collection choice.
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer asserts the true zero-alloc
claims exactly and records (with a loose regression bound, not a false zero)
the unrelated Entity-level cost on the full HitProbability path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:30:30 +02:00
// Recorded, not asserted zero: the full gated-in HitProbability call still allocates, from Entity's own
// collection enumeration (GetBehavior<T> over ReactiveCollection<T>), not from anything this fix batch
// touches. A loose upper bound catches a real regression (e.g. a reintroduced comparer wrapper) without
// pretending Entity's own cost is Lane's or Silhouette's to fix here.
for ( var i = 0 ; i < 100 ; i + + ) FireControl . HitProbability ( e . Weapon , e . Shooter , e . Target ) ;
2026-09-29 04:17:43 +02:00
var before = GC . GetAllocatedBytesForCurrentThread ( ) ;
Cut 12.2 Soul fix batch 2, S3: measure allocations, correct an overclaiming comment
Measured (Release, 7x9 solid hull -- the LonginusX shipped-hull fixture the
brief named is not available to this test file):
- Lane: 0 B / 1000 calls (was: 64 B/call from Array.Sort's comparer wrapper;
now confirmed 0 after the earlier IComparable<T> change in this batch).
- Silhouette (caller-supplied buffer): 0 B / 1000 calls.
- HitProbability, gated in (a real, non-gated-out call): ~120 B/call. This is
NOT from Lane, Silhouette or the comparer -- it comes from Accuracy/
Resolution's own GetBehavior<T>, which walks Entity.Equipment and each
item's Behaviors (both ReactiveCollection<T>); that type's enumerator is
heap-allocated per foreach. This predates Cut 12.2 and is out of this cut's
scope to fix.
- Inspect, gated in: ~280 B/call (same cause, more GetBehavior<T> calls).
The Forecast comment claiming "the forecast path (HitProbability, Inspect) no
longer allocates on a gated-in call" was not true and is corrected to say what
was actually measured: Silhouette itself is allocation-free given a pooled
buffer, and the residual cost belongs to Entity's own collection choice.
LaneAndSilhouetteAllocateNothingGivenAPooledBuffer asserts the true zero-alloc
claims exactly and records (with a loose regression bound, not a false zero)
the unrelated Entity-level cost on the full HitProbability path.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 03:30:30 +02:00
var sum = 0f ;
for ( var i = 0 ; i < 1000 ; i + + ) sum + = FireControl . HitProbability ( e . Weapon , e . Shooter , e . Target ) ;
var hitProbabilityAllocated = GC . GetAllocatedBytesForCurrentThread ( ) - before ;
Assert . True ( sum > 0f , "fixture: this must be a real, gated-in call, not a gated-out one" ) ;
Assert . True ( hitProbabilityAllocated < 500_000 , $"1000 gated-in HitProbability calls allocated {hitProbabilityAllocated} bytes -- well above Entity's own ~120 B/call baseline; likely a reintroduced Lane/Silhouette allocation" ) ;
}
Cut 12.2: where a hit lands is priced and drawn from one silhouette
FireControl.Silhouette replaces HullKernel/WeightedPick/POnHull: the hull's
lateral shadow at a bearing, merged into intervals, integrated exactly with
CultMath.erf/erfinv instead of a discrete per-cell Gaussian sum. FireControl.Lane
does exact slab traversal to pick the impact cell from a lateral offset; only
its first element (the entry-nearest occupied cell) is used here, per the cut
map -- the rest is 12.3's armour-first absorption march.
Bearing timing: PendingShot.PBase is renamed PFire and now carries only the
shooter-side factor (Accuracy x PSensor) behind the gates; TravelDirection,
Spread and FireRange are frozen alongside it. The target's facing is read live
at Commit through FireControl.CommitProbability (steps 1-5 of the spec's
"Bearing timing"), not frozen at Fire, so a target that turns its armour into
an incoming shot takes the hit there. ShotOutcome gains Bearing/Lateral,
frozen at commit; Apply reads only those, never a live position or facing.
Entity.ApplyHit takes an already-schematic bearing instead of a world
direction, and the ToSchematic call 12.1 put inside it is gone.
Superseded tests: PlacementAndProbabilityShareOneKernel, EveryHitLandsOnMetal
and AimingAtTheSternHitsTheStern are replaced by
FireControlCut12Tests.PlacementAndProbabilityShareOneSilhouette,
HitsLandOnTheFacingEdge and AimingAtAnItemCentresTheScatterOnItsLane.
FireAuthorityTests.AimedHitLandsOnSelectedItem and PenetrationMarchIsPlanar are
updated for ApplyHit's new bearing parameter and Lane's entry-nearest-cell
placement (a solid hull's Tool item is never itself the entry cell; reaching
it now needs the same penetration march the new tests exercise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 20:40:50 +02:00
}