- commit
- c859266c26433dfe4a5209fb21d050848509798c
- parent
- fbc47ac64b37aed2c5ed3c2b029678688c4dbf6f
- author
- Dami <dami.etoile@gmail.com>
| M | src/ | 41 | +++++++++++++++++++++++++----- |
| M | tests/ | 38 | ++++++++++++++++++++++++++++ |
| M | tests/ | 2 | +- |
@@ -171,8 +171,9 @@ export interface CellRegion {
171171 /**172172 * What the field knows about the track at a cell: distance to the nearest173173 * track cell and the grid direction toward it (as a piece rotation, so facade174- * pieces like fans can face the track). `dist` is Infinity beyond175- * ORACLE_HORIZON — those cells are the ambient band.174+ * pieces like fans can face the track). `dist` is true out to175+ * ORACLE_GRADE_HORIZON (the graded ambient falloff needs it) and Infinity176+ * beyond; everything past 5.5 is the ambient band.176177 */177178 export interface TrackProbe {178179 dist: number;@@ -188,11 +189,20 @@ export type TrackOracle = (x: number, z: number) => TrackProbe;
188189 189190 /**190191 * How far (in cells) an oracle must see: the banded rules stop at 5.5, so any191- * track farther than this cannot change a cell's outcome. Chunked backers192- * need neighbor context of exactly this radius.192+ * track farther than this cannot change a cell's BAND. Chunked backers193+ * need neighbor context of exactly ORACLE_GRADE_HORIZON.193194 */194195 export const ORACLE_HORIZON = 6;195196 197+/**198+ * Widened oracle contract (M13, the promised distance-graded ambient):199+ * oracles return TRUE distances out to this radius (matching the200+ * single-track margin) and saturate to Infinity beyond — so the ambient201+ * band can thin with distance instead of a flat factor. This widens the202+ * CONTRACT, not the field rules: the graded density is the only consumer.203+ */204+export const ORACLE_GRADE_HORIZON = 13;205+196206 /** Track bounds + margin — the single-track region. */197207 export function regionAround(198208 cells: ReadonlySet<string>,@@ -219,8 +229,8 @@ export function cellSetOracle(cells: ReadonlySet<string>): TrackOracle {
219229 return (x, z) => {220230 let best = Infinity;221231 let bx = 0, bz = 1;222- for (let dx = -ORACLE_HORIZON; dx <= ORACLE_HORIZON; dx++) {223- for (let dz = -ORACLE_HORIZON; dz <= ORACLE_HORIZON; dz++) {232+ for (let dx = -ORACLE_GRADE_HORIZON; dx <= ORACLE_GRADE_HORIZON; dx++) {233+ for (let dz = -ORACLE_GRADE_HORIZON; dz <= ORACLE_GRADE_HORIZON; dz++) {224234 if (cells.has(`${x + dx},${z + dz}`)) {225235 const d = Math.hypot(dx, dz);226236 if (d < best) {@@ -245,6 +255,21 @@ export function cellSetOracle(cells: ReadonlySet<string>): TrackOracle {
245255 246256 /** Ambient band thins the field out toward the fog, not a hard cliff. */247257 const AMBIENT_DENSITY = 0.3;258+/** Graded falloff floor at/beyond the grade horizon — the last stragglers259+ * the fog eats. */260+const AMBIENT_FLOOR = 0.05;261+262+/** Distance-graded ambient density (M13): 0.3 at the far-band edge263+ * thinning linearly to the floor at ORACLE_GRADE_HORIZON. Saturated264+ * distances (Infinity) sit at the floor. */265+export function ambientDensity(dist: number): number {266+ const span = ORACLE_GRADE_HORIZON - 5.5;267+ const t = Math.max(268+ 0,269+ Math.min(1, (Math.min(dist, ORACLE_GRADE_HORIZON) - 5.5) / span),270+ );271+ return AMBIENT_DENSITY + (AMBIENT_FLOOR - AMBIENT_DENSITY) * t;272+}248273 249274 export function placeDecor(250275 rng: Rng,@@ -280,7 +305,9 @@ export function placeDecor(
280305 // The ambient band reuses the far-band rules, thinned: horizon fill of281306 // the same trees/rocks/cliffs, sparse enough that the fog does the rest.282307 const ruleBand: Band = band === "ambient" ? "far" : band;283- if (band === "ambient" && !cellRng.chance(AMBIENT_DENSITY)) continue;308+ if (band === "ambient" && !cellRng.chance(ambientDensity(probe.dist))) {309+ continue;310+ }284311 285312 // Weighted union of setting groups and marble-kit decor for this band.286313 const options: (readonly [() => Placement | null, number])[] = [];@@ -96,6 +96,44 @@ Deno.test("ambient band: decor continues beyond the old far cutoff (5.5)", () =>
9696 assert(ambient > 0, "expected some ambient-band decor beyond dist 5.5");9797 });9898 99+Deno.test("ambient falloff: the field thins with distance (graded, not flat)", () => {100+ // M13: ambient density grades from 0.3 at the far edge to the floor at101+ // the grade horizon. Aggregate per-cell hit rates over two annuli —102+ // the near ambient ring must be denser than the deep ring.103+ const full = regionAround(RING, 13);104+ let nearCells = 0;105+ let deepCells = 0;106+ for (let x = full.minX; x <= full.maxX; x++) {107+ for (let z = full.minZ; z <= full.maxZ; z++) {108+ let d = Infinity;109+ for (const cell of RING) {110+ const [cx, cz] = cell.split(",").map(Number);111+ d = Math.min(d, Math.hypot(x - cx, z - cz));112+ }113+ if (d > 5.5 && d <= 8) nearCells++;114+ else if (d > 10.5) deepCells++;115+ }116+ }117+ let nearHits = 0;118+ let deepHits = 0;119+ for (let i = 0; i < 40; i++) {120+ for (const p of field(`grade-${i}`)) {121+ const d = distToRing(p);122+ if (d > 5.5 && d <= 8) nearHits++;123+ else if (d > 10.5) deepHits++;124+ }125+ }126+ const nearRate = nearHits / (nearCells * 40);127+ const deepRate = deepHits / (deepCells * 40);128+ assert(nearHits > 0, "no near-ambient decor at all");129+ assert(130+ nearRate > deepRate * 1.8,131+ `falloff too flat: near ${nearRate.toFixed(4)} vs deep ${132+ deepRate.toFixed(4)133+ }`,134+ );135+});136+99137 Deno.test("no decor lands on or within 1 cell of the track", () => {100138 for (let i = 0; i < 10; i++) {101139 for (const p of field(`clip-${i}`)) {