Ledger #6 fix + the round's one sanctioned gig-golden re-pin, and ledger #7. Classic/synthetic golden 0x3fa36874 frozen (classic boot byte-identical across all seeds); base plan untouched. - Corner poster: the R15 finding (frontage poster near a crossing kerb) is NOT fixable by the brief's 'nudge along the facade' — proved the crossing edge is PARALLEL and spans the facade (0/187 slides help); band_room/rsl land on through-lots on narrow warehouse blocks whose +Z wall (B's door side) overlaps the block's other street by up to ~3 m. Surfaced to Fable; ruling: bias pickVenues. It now prefers a band_room/rsl candidate whose +Z facade clears every carriageway by >= POSTER_CLEAR (0.5 m), broadening off-spine if the fringe is uniformly narrow. Result: 0/851 frontage posters below floor across 400 seeds (worst 0.51 m); pub untouched (hero shot unmoved). - Gig golden re-pinned 0x4f4a549d -> 0xb1d48ea1 (posters moved). selfcheck's R15 blanket frontage exemption becomes a real floor: frontage >= POSTER_CLEAR, spine >= 0 (POSTER_CLEAR exported from the barrel). Mutation-tested non-vacuous. - No-spine osm fixture 'silverton' (single-row town, 0 main edges) proves pickVenues' no-spine branch + buildPosters' no-main fallback, closing the R14 sweep gap. Base golden pinned 0xd4b351c9; sweep asserts 0 main edges + graceful 2-4-venue placement. selfcheck ALL GREEN 14769/14769; scaffold + consistency GREEN. - CITY_SPEC v3 amended (golden, poster clearance-bias prose, no-spine note, recorded amendment). Left the prime-flag-law blockquote for F (the v3.1 classic-law + flags-table is F's amendment). For F: default-boot golden = 0xb1d48ea1; ?classic=1 target = 0x3fa36874. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
54a030b38b
commit
2b13dd4ad7
@ -1,10 +1,37 @@
|
||||
# LANE A — CITYGEN · progress (PROCITY-A)
|
||||
|
||||
*Status: **all deliverables landed; self-check all-green (14264/14264); rounds 2–15 closed** (see below).
|
||||
v3.0 release close-out: frontage-poster +Z flip (ledger #1) + the one sanctioned gig-golden move
|
||||
(`0x1f636349 → 0x4f4a549d`); synthetic golden `0x3fa36874` frozen.*
|
||||
*Status: **all deliverables landed; self-check all-green (14769/14769); rounds 2–16 closed** (see below).
|
||||
v3.1 (THE FLIP): corner-poster fix via `pickVenues` +Z-clearance bias (ledger #6, gig golden
|
||||
`0x4f4a549d → 0xb1d48ea1`) + a no-spine osm fixture (silverton, #7). Classic/synthetic `0x3fa36874` frozen.*
|
||||
*Date: 2026-07-16 · owner: PROCITY-A (Opus 4.8)*
|
||||
|
||||
## Round 16 (2026-07-16) — v3.1 THE FLIP: corner-poster clearance bias (#6) + no-spine osm fixture (#7)
|
||||
|
||||
Lane A ran **first** among the parallel lanes (F bookends with the flip; A's golden feeds F's default-boot
|
||||
gate). The R15 finding was that a corner venue's frontage poster can sit near a crossing kerb.
|
||||
|
||||
Delivered (one commit):
|
||||
1. **Corner-poster fix — via `pickVenues`, not a nudge.** I proved the brief's proposed fix (nudge along
|
||||
the facade) is geometrically impossible: the crossing edge is **parallel** to the facade and spans it
|
||||
(0/187 along-facade slides help). Root cause: band_room/rsl land on through-lots on narrow warehouse
|
||||
blocks, so the `+Z` wall (B's door side) faces the block's other street, overlapping its carriageway by
|
||||
up to ~3 m. **Surfaced to Fable; ruling: bias `pickVenues`.** It now prefers a band_room/rsl candidate
|
||||
whose `+Z` facade clears every carriageway by ≥ `POSTER_CLEAR` (0.5 m), broadening off-spine when the
|
||||
fringe is uniformly narrow. Result: **0/851** frontage posters below floor across 400 seeds (worst
|
||||
0.51 m); pub untouched (hero shot unmoved). Feasibility pre-checked: a clear lot always exists.
|
||||
2. **Gig golden re-pinned `0x4f4a549d → 0xb1d48ea1`** — the sanctioned move (posters moved); synthetic/
|
||||
classic `0x3fa36874` frozen. §v3 FROZEN marker records the amendment.
|
||||
3. **Selfcheck: exemption → floor.** R15's blanket frontage exemption is now a real invariant — frontage
|
||||
posters must clear their kerb by ≥ `POSTER_CLEAR` (exported from the barrel), spine posters keep ≥ 0.
|
||||
4. **No-spine osm fixture `silverton`** (ledger #7): a constructed single-row town (all shops one latitude
|
||||
⇒ 0 `main` edges). Closes the R14 gap (both real fixtures had a spine); proves `pickVenues`' no-spine
|
||||
branch + `buildPosters`' no-main fallback. Base golden pinned `0xd4b351c9`; sweep asserts 0 main edges +
|
||||
graceful 2–4-venue placement. Selfcheck **ALL GREEN 14769/14769**; scaffold + consistency GREEN.
|
||||
5. **CITY_SPEC §v3 amended** (golden line, poster clearance-bias prose, no-spine note, recorded amendment).
|
||||
Left the prime-flag-law blockquote for F (the v3.1 classic-law + flags-table is F's §v3 amendment).
|
||||
|
||||
For F: default-boot golden = `0xb1d48ea1`; `?classic=1` regression target = `0x3fa36874`.
|
||||
|
||||
## Round 15 (2026-07-16) — the frontage-poster +Z flip (ledger #1) + one sanctioned golden move
|
||||
|
||||
Lane A ran **first** (B's hero re-shoot + F's tour hang off it). The R14 review found every venue's
|
||||
|
||||
@ -218,6 +218,13 @@ night in v1; interior-mapping shader is a stretch goal).
|
||||
> facade (it had been on the `−Z` back, invisible from the street); this moved `plan.posters`, so the gig
|
||||
> golden re-pinned `0x1f636349 → 0x4f4a549d` in the same commit under the amendment law above. The
|
||||
> synthetic golden `0x3fa36874` did not move — this is the round's one sanctioned gig-golden move.)*
|
||||
>
|
||||
> *(**Recorded amendment — ROUND16, ledger #6:** on a narrow through-lot a band_room/rsl's `+Z` facade
|
||||
> can abut the block's *other* street, putting its frontage poster over that carriageway. `pickVenues`
|
||||
> now **prefers a band_room/rsl lot whose facade clears every carriageway by ≥ `POSTER_CLEAR`** (0.5 m;
|
||||
> broadens off-spine if the warehouse fringe is uniformly narrow). This moved `plan.posters`, so the gig
|
||||
> golden re-pinned `0x4f4a549d → 0xb1d48ea1`; synthetic `0x3fa36874` still frozen. The v3.1 default flip
|
||||
> + classic law is F's amendment to this section — see the flags table.)*
|
||||
|
||||
> **Prime flag law.** The gig layer is a **post-hoc augmentation** applied by the selector only when
|
||||
> `?gigs=1`. With the flag OFF, `generatePlanFor(seed, src)` returns the byte-identical v2 base plan —
|
||||
@ -286,8 +293,10 @@ Only **tonight's** (night 0) playing venues are advertised. Each such venue gets
|
||||
frontage** — seated on the **+Z street facade** (B's canon: `buildings.js` facade + `venue.js` door/queue/
|
||||
camera are all `+Z`; position `lot + (sin ry, cos ry)·(d/2+ε)`), turned to read *from* the street
|
||||
(`ry = lot.ry + π` so its printed face points `+Z` at the queue-side camera — **ROUND15 ledger #1**; before
|
||||
R15 it was on the `−Z` back and invisible from the street). Plus a seeded run along the spine — 2 per main
|
||||
edge, at `t ∈ [0.2,0.8]` of the block, on **either verge**. Different venues' posters share the same pole
|
||||
R15 it was on the `−Z` back and invisible from the street). So the poster never sits over a back-street's
|
||||
bitumen, **`pickVenues` prefers a band_room/rsl lot whose facade clears every carriageway by ≥ `POSTER_CLEAR`**
|
||||
(ROUND16 ledger #6 — exported from the barrel; the selfcheck asserts the same floor). Plus a seeded run along
|
||||
the spine — 2 per main edge (or, when a town has no main spine, every edge), at `t ∈ [0.2,0.8]` of the block, on **either verge**. Different venues' posters share the same pole
|
||||
runs — that IS the district reading. `ry` follows the house convention (a poster's printed face =
|
||||
`(−sin ry, −cos ry)`, so `atan2` of the *away-from-viewer* normal); B places E's poster skin at `(x, z)`
|
||||
rotated `ry`; F wires "poster → that venue's tonight-gig".
|
||||
@ -316,9 +325,11 @@ by seed, spread **night-major across the whole week's slots town-wide** (not one
|
||||
canonical half-open predicate (`open ≤ hour < close`) for D's gig-night patronage surge and everyone else.
|
||||
|
||||
**Gig golden** (guards gig-layer drift; pure-generator path, no custom file), seed `20261990` synthetic:
|
||||
**`0x4f4a549d`** (**ROUND15 re-pin** — the frontage-poster +Z flip, ledger #1, moved `plan.posters`; was
|
||||
`0x1f636349` the R13 district, `0xa6ae5a5e` the R12 alpha). Asserted by `selfcheck.js` alongside the frozen
|
||||
v2 goldens; changing gig output re-pins it.
|
||||
**`0xb1d48ea1`** (**ROUND16 re-pin** — the band_room/rsl facade-clearance bias, ledger #6, moved
|
||||
`plan.posters`; was `0x4f4a549d` the R15 +Z flip, `0x1f636349` the R13 district, `0xa6ae5a5e` the R12
|
||||
alpha). Asserted by `selfcheck.js` alongside the frozen v2/classic golden; changing gig output re-pins it.
|
||||
The gig sweep now also runs a **no-spine osm fixture** (`silverton`, single-row) proving `pickVenues`
|
||||
places venues with no main spine (ROUND16 ledger #7).
|
||||
|
||||
## Shop types v1 (registry lives in `web/js/core/registry.js`, Lane A writes it, all lanes read)
|
||||
|
||||
|
||||
@ -2,6 +2,41 @@
|
||||
|
||||
Durable answers to questions other lanes raised against Lane A. Full status in `A-progress.md`.
|
||||
|
||||
## Round 16 (2026-07-16) — corner-poster fix via pickVenues clearance bias (ledger #6) + no-spine osm fixture (#7)
|
||||
|
||||
### → B + F: **new gig golden `0x4f4a549d → 0xb1d48ea1`** (classic/synthetic `0x3fa36874` unchanged)
|
||||
The round's one sanctioned move. **F**: the default-boot gate pins to `0xb1d48ea1` (not the R15 value the
|
||||
brief cited). **B**: re-shoot against this. Amendment law; §v3 marker records it.
|
||||
|
||||
### The corner-poster fix (NOT a nudge — Fable's brief premise was wrong)
|
||||
The R15 finding was that a corner venue's frontage poster sits near a crossing kerb. The brief proposed
|
||||
nudging the poster ALONG the facade away from the crossing-street *end* — but I proved that's geometrically
|
||||
impossible: the crossing edge is **parallel** to the facade and spans it (0/187 along-facade slides recover
|
||||
any clearance). Root cause: band_room/rsl land on **through-lots on narrow warehouse-fringe blocks**, so the
|
||||
`+Z` wall (B's door side, where the poster seats) faces the block's *other* street, overlapping its
|
||||
carriageway by up to ~3 m. **Fable's ruling (asked mid-round): bias `pickVenues`.** So:
|
||||
- `gigs.js pickVenues` now prefers a band_room/rsl candidate whose `+Z` facade clears every carriageway by
|
||||
**≥ `POSTER_CLEAR` (0.5 m)**; if the warehouse fringe is uniformly narrow it broadens to any off-spine
|
||||
clear lot before falling back. Proven: **0 of 851** frontage posters across 400 seeds now sit below the
|
||||
floor (worst clearance 0.51 m). Pub is untouched (spine-fronted, `+Z` open — the hero shot doesn't move).
|
||||
- The R15 blanket frontage *exemption* in the selfcheck is now a real **floor**: frontage posters must clear
|
||||
their kerb by ≥ `POSTER_CLEAR`; spine posters keep the ≥ 0 no-bitumen floor. `POSTER_CLEAR` is exported
|
||||
from the barrel — import it, don't re-guess. (`−0.02` in the assert absorbs r2 poster-coord rounding.)
|
||||
|
||||
### The no-spine osm fixture (ledger #7): `silverton`
|
||||
`osm_fixture.js` gains **`silverton`** — a *constructed* single-row town (all shops at one latitude ⇒
|
||||
plan_osm bins them into one avenue ⇒ **0 `main` edges**). Closes the R14 sweep gap (both real fixtures had a
|
||||
spine): it proves `pickVenues`' no-spine branch (pub's `onMain` filter is empty) and `buildPosters`' no-main
|
||||
fallback (spine run over every edge). Base golden pinned `0xd4b351c9`; the sweep asserts 0 main edges +
|
||||
graceful 2–4-venue placement. `osmTownKeys()` now returns 3 towns — anything iterating it (shell selector,
|
||||
your gates) picks it up; it's a valid town, `?plansrc=osm&town=silverton`.
|
||||
|
||||
### → F: the classic-law / flags-table §v3 amendment is yours
|
||||
I amended §v3 for the golden + poster bias only (localized, recorded in the FROZEN marker). The v3.1 flip
|
||||
(default-on, `?classic=1` covenant, the flags table) is your §v3 amendment — I left the prime-flag-law
|
||||
blockquote untouched to avoid racing you on the same lines. My classic golden (`0x3fa36874`) is your
|
||||
`?classic=1` regression target; my gig golden (`0xb1d48ea1`) is your default-boot target.
|
||||
|
||||
## Round 15 (2026-07-16) — the frontage-poster +Z flip (ledger #1) + the one sanctioned golden move
|
||||
|
||||
### → B + F: **new gig golden `0x1f636349 → 0x4f4a549d`** (synthetic `0x3fa36874` unchanged)
|
||||
|
||||
@ -10,12 +10,26 @@
|
||||
// → town-wide. See LANE_A_NOTES §Round 13 for the interface C/D/B/F hang off.
|
||||
|
||||
import { rng, seedFor, mulberry32, pick, shuffle, irange } from '../core/prng.js';
|
||||
import { SHOP_TYPES, poleOffset } from '../core/registry.js';
|
||||
import { SHOP_TYPES, poleOffset, roadWidth } from '../core/registry.js';
|
||||
import { shopName, bandName } from './names.js';
|
||||
|
||||
const r2 = v => Math.round(v * 100) / 100;
|
||||
const r4 = v => Math.round(v * 10000) / 10000;
|
||||
|
||||
// A venue's own frontage poster seats on the +Z street facade, `d/2 + FACADE_PROUD` out from the lot
|
||||
// centre (shared by pickVenues' clearance test and buildPosters). ROUND16 (ledger #6): band_room/rsl
|
||||
// selection prefers a lot whose facade clears every carriageway by ≥ POSTER_CLEAR, so the poster can
|
||||
// never sit on a back-street's bitumen (a narrow through-lot otherwise abuts the block's other street).
|
||||
// selfcheck asserts the same floor — import POSTER_CLEAR there, don't re-guess it.
|
||||
export const POSTER_CLEAR = 0.5;
|
||||
const FACADE_PROUD = 0.06;
|
||||
// nearest point on segment AB to P (same helper the selfcheck uses; kept local so gigs.js stays pure).
|
||||
const nearestSeg = (px, pz, ax, az, bx, bz) => {
|
||||
const dx = bx - ax, dz = bz - az, L2 = dx * dx + dz * dz || 1;
|
||||
let t = ((px - ax) * dx + (pz - az) * dz) / L2; t = Math.max(0, Math.min(1, t));
|
||||
return [ax + dx * t, az + dz * t];
|
||||
};
|
||||
|
||||
// A night gig: doors at DUSK (seg4), on during NIGHT (seg5) — see lighting.js segment→hour map.
|
||||
const NIGHTS = 7;
|
||||
const GIG_START_SEG = 5; // NIGHT (22:00) — gig is "on"
|
||||
@ -55,11 +69,25 @@ function pickVenues(plan, citySeed) {
|
||||
const blockById = new Map(plan.blocks.map(b => [b.id, b]));
|
||||
const mainEdges = new Set(plan.streets.edges.filter(e => e.kind === 'main').map(e => e.id));
|
||||
|
||||
const nodeById = new Map(plan.streets.nodes.map(n => [n.id, n]));
|
||||
const lotOf = s => lotById.get(s.lot);
|
||||
const blockKindOf = s => { const l = lotOf(s), b = l && blockById.get(l.block); return b ? b.kind : null; };
|
||||
const onMain = s => { const l = lotOf(s); return !!l && mainEdges.has(l.frontEdge); };
|
||||
const dist2 = s => { const l = lotOf(s); return l ? l.x * l.x + l.z * l.z : 0; };
|
||||
const farthestFirst = arr => arr.slice().sort((a, b) => dist2(b) - dist2(a) || a.id - b.id);
|
||||
// clearance of a shop's +Z frontage-poster point to the nearest carriageway (ROUND16 ledger #6).
|
||||
const facadeClear = s => {
|
||||
const l = lotOf(s); if (!l) return Infinity;
|
||||
const off = l.d / 2 + FACADE_PROUD;
|
||||
const px = l.x + Math.sin(l.ry) * off, pz = l.z + Math.cos(l.ry) * off;
|
||||
let best = Infinity;
|
||||
for (const e of plan.streets.edges) {
|
||||
const a = nodeById.get(e.a), b = nodeById.get(e.b); if (!a || !b) continue;
|
||||
const [nx, nz] = nearestSeg(px, pz, a.x, a.z, b.x, b.z);
|
||||
best = Math.min(best, Math.hypot(px - nx, pz - nz) - roadWidth(e) / 2);
|
||||
}
|
||||
return best;
|
||||
};
|
||||
|
||||
const base = plan.shops.filter(s => {
|
||||
if (s.openLate) return false; // must not displace the night landmark
|
||||
@ -93,6 +121,16 @@ function pickVenues(plan, citySeed) {
|
||||
if (far.length) cand = far;
|
||||
}
|
||||
}
|
||||
// ROUND16 ledger #6: keep the off-spine kinds off a back-street's bitumen. Prefer candidates whose
|
||||
// +Z facade clears every carriageway by ≥ POSTER_CLEAR; if the warehouse fringe is uniformly narrow,
|
||||
// broaden to ANY off-spine clear lot before falling back. (pub is spine-fronted — its +Z is open —
|
||||
// so it keeps its placement untouched; the hero shot doesn't move.) Always a clear lot exists in the
|
||||
// synthetic town; the fallback only ever fires on a degenerate/osm plan.
|
||||
if (kind === 'band_room' || kind === 'rsl') {
|
||||
let clear = cand.filter(s => facadeClear(s) >= POSTER_CLEAR);
|
||||
if (!clear.length) clear = pool.filter(s => !onMain(s) && facadeClear(s) >= POSTER_CLEAR);
|
||||
if (clear.length) cand = clear;
|
||||
}
|
||||
if (!cand.length) cand = pool; // last resort: anything left
|
||||
const shortlist = farthestFirst(cand).slice(0, Math.min(5, cand.length));
|
||||
const shop = pick(rng(citySeed, 'venuepick', i), shortlist);
|
||||
@ -208,7 +246,7 @@ function buildPosters(plan, citySeed, venues, gigs) {
|
||||
// read FROM the street: venue.js renders a poster's printed face along (−sin ry, −cos ry) = A's
|
||||
// outward normal, so ry = lot.ry + π points that face outward (else DoubleSide shows a mirrored name).
|
||||
const fx = Math.sin(lot.ry), fz = Math.cos(lot.ry); // +Z: the street facade (B's canon)
|
||||
const off = lot.d / 2 + 0.06; // out onto the facade plane, proud of it
|
||||
const off = lot.d / 2 + FACADE_PROUD; // out onto the facade plane, proud of it
|
||||
add(lot.x + fx * off, lot.z + fz * off, lot.ry + Math.PI, gig.gigId);
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
export { generatePlan, chunkIndex, chunkKey, CHUNK, lotCorners, obbOverlap, isOpen } from './plan.js';
|
||||
export { shopName, townName, bandName } from './names.js';
|
||||
export { generatePlanOSM, osmTownKeys } from './plan_osm.js';
|
||||
export { withGigs, gigKeyFor } from './gigs.js';
|
||||
export { withGigs, gigKeyFor, POSTER_CLEAR } from './gigs.js';
|
||||
// The street-corridor law + venue vocabulary, re-exported so consumers get the whole Lane A contract
|
||||
// from one import (ROUND13). `roadWidth`/`vergeBand`/`poleOffset` are the road-vs-verge split of
|
||||
// `edge.width` — see the note in registry.js; `gigKeyFor` is the one and only genre→audio-key mapping.
|
||||
|
||||
@ -134,6 +134,30 @@ export const OSM_TOWNS = {
|
||||
{"id":11242974232,"name":"Sanctus Catholic Books and Gift Shop","type":"book","lat":-33.715718,"lon":150.31095,"suburb":""}
|
||||
],
|
||||
},
|
||||
// ROUND16 (ledger #7): a CONSTRUCTED single-row town — every shop shares one latitude, so plan_osm
|
||||
// bins them into ONE avenue and builds NO main spine (spineNodes.length === 1 ⇒ 0 'main' edges). Unlike
|
||||
// Melbourne/Katoomba (8 lat rows ⇒ a multi-node spine), this proves the no-spine placement branch of
|
||||
// gigs.js pickVenues + the no-main-spine poster fallback in buildPosters. Not a scraped cluster; the
|
||||
// names are flavour (Silverton is a real one-street NSW town). ids are synthetic (9-prefixed) seeds.
|
||||
silverton: {
|
||||
town: "Silverton",
|
||||
source: "constructed single-row fixture (no-spine test, ROUND16 ledger #7)",
|
||||
center: {"lat":-31.8600,"lon":141.2255},
|
||||
shops: [
|
||||
{"id":9000001,"name":"Silverton Op Shop","type":"opshop","lat":-31.8600,"lon":141.2200,"suburb":"Silverton"},
|
||||
{"id":9000002,"name":"Old School Book Exchange","type":"book","lat":-31.8600,"lon":141.2210,"suburb":"Silverton"},
|
||||
{"id":9000003,"name":"Outback Vinyl","type":"record","lat":-31.8600,"lon":141.2220,"suburb":"Silverton"},
|
||||
{"id":9000004,"name":"Silverton Video","type":"video","lat":-31.8600,"lon":141.2230,"suburb":"Silverton"},
|
||||
{"id":9000005,"name":"Desert Trading Post","type":"pawn","lat":-31.8600,"lon":141.2240,"suburb":"Silverton"},
|
||||
{"id":9000006,"name":"Mundi Collectables","type":"toy","lat":-31.8600,"lon":141.2250,"suburb":"Silverton"},
|
||||
{"id":9000007,"name":"CWA Op Shop","type":"opshop","lat":-31.8600,"lon":141.2260,"suburb":"Silverton"},
|
||||
{"id":9000008,"name":"Ghost Town Books","type":"book","lat":-31.8600,"lon":141.2270,"suburb":"Silverton"},
|
||||
{"id":9000009,"name":"Silverton Corner Store","type":"milkbar","lat":-31.8600,"lon":141.2280,"suburb":"Silverton"},
|
||||
{"id":9000010,"name":"The Old Wares","type":"pawn","lat":-31.8600,"lon":141.2290,"suburb":"Silverton"},
|
||||
{"id":9000011,"name":"Dusty Grooves","type":"record","lat":-31.8600,"lon":141.2300,"suburb":"Silverton"},
|
||||
{"id":9000012,"name":"Salvos Silverton","type":"opshop","lat":-31.8600,"lon":141.2310,"suburb":"Silverton"}
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const DEFAULT_TOWN = "melbourne";
|
||||
|
||||
@ -9,7 +9,7 @@ import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { generatePlan, chunkIndex, lotCorners, obbOverlap, CHUNK } from './plan.js';
|
||||
import { generatePlanOSM, osmTownKeys } from './plan_osm.js';
|
||||
import { generatePlanFor, gigKeyFor } from './index.js';
|
||||
import { generatePlanFor, gigKeyFor, POSTER_CLEAR } from './index.js';
|
||||
import { allFacadeSkins, SHOP_TYPES, VENUE_KINDS, genreForVenueKind, roadWidth } from '../core/registry.js';
|
||||
import { xmur3 } from '../core/prng.js';
|
||||
|
||||
@ -190,25 +190,24 @@ function districtInvariants(plan, label) {
|
||||
}
|
||||
ok(clash === null, `${label}: no band plays two venues on one night` + (clash !== null ? ` (night ${clash})` : ''));
|
||||
|
||||
// ── posters: town-wide, on the verge, never on the bitumen (debt #5) ──
|
||||
// ── posters: town-wide, never on the bitumen — with a real FRONTAGE floor (ROUND16 ledger #6) ──
|
||||
const gigIds = new Set(plan.gigs.map(g => g.gigId));
|
||||
ok(Array.isArray(plan.posters) && plan.posters.every(p => gigIds.has(p.gigId) && isFiniteNum(p.x) && isFiniteNum(p.z) && isFiniteNum(p.ry)), `${label}: posters reference a real gig with finite coords`);
|
||||
ok(plan.posters.every(p => plan.gigs.find(g => g.gigId === p.gigId).night === 0), `${label}: posters advertise tonight's gigs`);
|
||||
// The no-bitumen rule (the R12 regression: spine posters at node.x+2) is for FREE-STANDING verge
|
||||
// posters. A venue's own FRONTAGE poster is bound to the building B draws — since R15 it sits on the
|
||||
// +Z street facade (B's canon: buildings.js facade + venue.js door/queue/camera are all +Z). A corner
|
||||
// venue whose facade abuts a crossing street legitimately seats its poster near that street's kerb —
|
||||
// B's building geometry, not a mid-road defect — so frontage posters are exempt here and validated by
|
||||
// the placement asserts above (they reference tonight's gigs with finite coords).
|
||||
// Two poster classes, two floors. A FRONTAGE poster sits on its venue's +Z street facade (B's canon:
|
||||
// buildings.js facade + venue.js door/queue/camera are +Z); ROUND16 pickVenues now PREFERS a band_room/
|
||||
// rsl lot whose facade clears every carriageway, so — no more R15 blanket exemption — a frontage poster
|
||||
// must clear its nearest kerb by ≥ POSTER_CLEAR. A SPINE poster is free-standing on the verge and keeps
|
||||
// the plain no-bitumen floor (≥ 0, the R12 regression guard). −0.02 absorbs r2 poster-coord rounding vs
|
||||
// pickVenues' un-rounded clearance test (worst observed frontage clearance is ~0.51 m).
|
||||
const lotByIdD = new Map(plan.lots.map(l => [l.id, l]));
|
||||
const onFacade = p => venues.some(v => {
|
||||
const lot = lotByIdD.get(v.lot); if (!lot) return false;
|
||||
const off = lot.d / 2 + 0.06; // gigs.js buildPosters item 1
|
||||
const off = lot.d / 2 + 0.06; // gigs.js buildPosters item 1 (FACADE_PROUD)
|
||||
return Math.hypot(p.x - (lot.x + Math.sin(lot.ry) * off), p.z - (lot.z + Math.cos(lot.ry) * off)) < 0.5;
|
||||
});
|
||||
let onRoad = null;
|
||||
for (const p of plan.posters) {
|
||||
if (onFacade(p)) continue; // frontage poster: on the venue building, not the verge
|
||||
let best = Infinity, bestEdge = null;
|
||||
for (const e of plan.streets.edges) {
|
||||
const a = nodeById2(plan, e.a), b = nodeById2(plan, e.b);
|
||||
@ -217,11 +216,13 @@ function districtInvariants(plan, label) {
|
||||
const d = Math.hypot(p.x - nx, p.z - nz);
|
||||
if (d < best) { best = d; bestEdge = e; }
|
||||
}
|
||||
// −1e-6 immunises the exact kerb-line boundary against r2 coord rounding (only reachable on a
|
||||
// no-main-spine fallback where poleOffset == roadWidth/2; never hit by the current sweep).
|
||||
if (bestEdge && best < roadWidth(bestEdge) / 2 - 1e-6) { onRoad = [p.id, bestEdge.id, +best.toFixed(2)]; break; }
|
||||
if (!bestEdge) continue;
|
||||
const frontage = onFacade(p);
|
||||
const clearance = best - roadWidth(bestEdge) / 2;
|
||||
const floor = frontage ? POSTER_CLEAR - 0.02 : -1e-6;
|
||||
if (clearance < floor) { onRoad = [p.id, bestEdge.id, +clearance.toFixed(2), frontage ? 'frontage' : 'spine']; break; }
|
||||
}
|
||||
ok(onRoad === null, `${label}: no poster stands on a carriageway` + (onRoad ? ` (poster ${onRoad[0]} is ${onRoad[2]}m from edge ${onRoad[1]}'s centreline)` : ''));
|
||||
ok(onRoad === null, `${label}: every poster clears its kerb (frontage ≥ ${POSTER_CLEAR} m, spine ≥ 0)` + (onRoad ? ` (${onRoad[3]} poster ${onRoad[0]} clears edge ${onRoad[1]} by only ${onRoad[2]} m)` : ''));
|
||||
}
|
||||
const nodeById2 = (plan, id) => plan.streets.nodes.find(n => n.id === id);
|
||||
|
||||
@ -271,7 +272,7 @@ for (const s of SEEDS) {
|
||||
// (round 8) Real-data towns get the same coverage as synthetic; only the synthetic-brief checks are
|
||||
// skipped. Goldens are pinned per town; the synthetic golden above stays 0x3fa36874.
|
||||
section('OSM plan source parity (fixture-driven, zero-network)');
|
||||
const OSM_GOLDENS = { melbourne: 0x34cfdec0, katoomba: 0x0f652510 };
|
||||
const OSM_GOLDENS = { melbourne: 0x34cfdec0, katoomba: 0x0f652510, silverton: 0xd4b351c9 };
|
||||
const OSM_SEEDS = [20261990, 1, 42, 777];
|
||||
for (const town of osmTownKeys()) {
|
||||
const report = {};
|
||||
@ -307,7 +308,7 @@ ok(JSON.stringify(generatePlanFor(42, 'synthetic', { gigs: true })) === JSON.str
|
||||
ok(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true, customBands: custom })) === JSON.stringify(pc), `custom bands: deterministic given (seed, list)`);
|
||||
}
|
||||
// gig-layer golden (guards gig-layer output drift; pure-generator path, no custom file)
|
||||
const GIG_GOLDEN = 0x4f4a549d; // ROUND15 re-pin: frontage posters flipped to the +Z street face (ledger #1 bug fix). Was 0x1f636349 (R13 district), 0xa6ae5a5e (R12 alpha).
|
||||
const GIG_GOLDEN = 0xb1d48ea1; // ROUND16 re-pin: pickVenues prefers band_room/rsl lots whose +Z facade clears crossing kerbs (ledger #6). Was 0x4f4a549d (R15 +Z flip), 0x1f636349 (R13 district), 0xa6ae5a5e (R12 alpha).
|
||||
{
|
||||
const hash = xmur3(JSON.stringify(generatePlanFor(20261990, 'synthetic', { gigs: true })))() >>> 0;
|
||||
if (GIG_GOLDEN === 0) console.log(` ⚠ gig golden UNPINNED → set GIG_GOLDEN = 0x${hash.toString(16).padStart(8, '0')}`);
|
||||
@ -333,14 +334,22 @@ for (const s of SEEDS) if (s > SWEEP_N) districtInvariants(generatePlanFor(s, 's
|
||||
ok([2, 3, 4].every(n => sweepVenueCounts.has(n)),
|
||||
`sweep: venue count spans {2,3,4} across ${SWEEP_N} seeds (saw {${[...sweepVenueCounts].sort((a, b) => a - b).join(',')}})`);
|
||||
|
||||
// osm graceful placement: katoomba is the smallest fixture (19 shops) with NO warehouse district, so
|
||||
// pickVenues' band_room/rsl rules hit their off-spine fallback — yet still land 2–4 valid venues incl.
|
||||
// a pub. (Both osm fixtures keep a main spine, so this proves the no-warehouse branch, not no-spine.)
|
||||
// osm graceful placement — two branches of pickVenues' degradation, each proven by a fixture that
|
||||
// genuinely lacks the structure the synthetic town has:
|
||||
// • katoomba (19 shops): NO warehouse district ⇒ band_room/rsl hit their off-spine fallback.
|
||||
// • silverton (single-row): NO main spine at all ⇒ pub's onMain filter is empty and buildPosters'
|
||||
// spine run falls back to every edge (ROUND16 ledger #7 — closes the R14 gap where both osm
|
||||
// fixtures had a spine). Both still land 2–4 valid venues incl. a pub.
|
||||
for (const seed of GIG_SEEDS) {
|
||||
const plan = generatePlanFor(seed, 'osm', { gigs: true, town: 'katoomba' });
|
||||
ok(plan.blocks.every(b => b.kind !== 'warehouse'),
|
||||
const kt = generatePlanFor(seed, 'osm', { gigs: true, town: 'katoomba' });
|
||||
ok(kt.blocks.every(b => b.kind !== 'warehouse'),
|
||||
`sweep osm/katoomba ${seed}: fixture has no warehouse district (graceful-placement precondition)`);
|
||||
districtInvariants(plan, `sweep osm/katoomba ${seed}`);
|
||||
districtInvariants(kt, `sweep osm/katoomba ${seed}`);
|
||||
|
||||
const sv = generatePlanFor(seed, 'osm', { gigs: true, town: 'silverton' });
|
||||
ok(!sv.streets.edges.some(e => e.kind === 'main'),
|
||||
`sweep osm/silverton ${seed}: fixture has no main spine (no-spine placement precondition)`);
|
||||
districtInvariants(sv, `sweep osm/silverton ${seed}`);
|
||||
}
|
||||
|
||||
// ── 4. every facade skin referenced by the registry exists on disk ──────────────────
|
||||
|
||||
Loading…
Reference in New Issue
Block a user