Files
magnus919_agent-skills/ascii-city-engine/README.md
Magnus HedemarkandGitHub 44809386a5 feat(ascii-city-engine): deep enrichment — street furniture, signage, dense Raleigh (#322)
* feat(ascii-city-engine): deep enrichment — street furniture, signage, dense Raleigh

Enriches the merged v0.1 skill with a dense, real-data streetscape.

Schema (backward-compatible): props gain optional label + provenance; new
optional signs layer for street-name text; buildings gain name/address/use.
world.schema.json admits signs; all v0.1 packs still validate.

Validator: per-kind prop glyph map with unknown-kind flag; sign validation;
signs included in content-bounds; v0.1 guards (isinstance crash-guard,
O(n^2) DoS caps) intact.

Engine: spatially-indexed prop billboards (signals, trees, crossings, transit
stops, bollards, benches, hydrants) depth-tested at terrain(x,y);
perspective-projected street-name sign text as an overlay pass; road
surface-material and lit ground styling; crosswalk bands; wayfinding HUD
naming the current street and the building faced. Reads spawn + first tile
from manifest.json.

Raleigh sample regenerated dense (same authoritative bbox): 159 buildings
(64 named, addresses, uses), 899 surfaces (86 named, surface/lit/lanes),
298 props across 7 kinds, 29 real street-name signs. 520K, under 2 MB.

Docs + evals: contract/engine-architecture/gis-ingestion/raleigh-poc updated
for the new layers and acceptance checks; 2 new evals (no fabricated signage;
props anchored to terrain).

Verified: validator 1410 PASS / 0 FAIL; broken fixture + AttributeError repro
exit 1; 5000-vertex DoS rejected in 31 ms; node --check OK; HTTP smoke 200
on engine/manifest/tile; validate-skills.rb 154 green; eval-coverage passes;
blocklist clean; headless-Chrome render shows readable in-world 'North
Wilmington Street' sign, signals, crosswalks, and on-street HUD.

AI-assisted contribution (Hermes Agent, spec-driven-development pipeline).

* fix(ascii-city-engine): address droid-review findings on PR #322

1. [P1] facingBuilding(): wrap the heading delta to [-pi,pi] before taking
   abs, so the Facing: HUD stops reporting a behind-the-camera building after
   the player turns past ~180 degrees.
2. [P2] buildIndices(): rasterize surface polyline edges into grid cells so
   long straight road segments register for surface styling and the On:
   HUD street name, instead of only indexing endpoint vertices.
3. [P1] validator: guard the new props kinds comprehension and the props/
   signs loops in all_points() against null, so 'props: null' or 'signs: null'
   produce a structured FAIL instead of an uncaught TypeError traceback.
4. [P3][security] validator: enforce the previously-declared
   MAX_FEATURES_PER_TILE (buildings+surfaces+props+signs) to keep the O(n^2)
   geometry and duplicate-ID paths bounded in aggregate.
5. [P3] engine: paint marked crossings as a distinct ground band (=) instead
   of a floating billboard, matching the documented crosswalk rendering.
6. [P3] eval: align prop-null-terrain expected_output with the render-time
   skip behavior actually implemented.

Verified: dense pack 1411/0 exit 0; broken/crash/dos/null packs all exit 1
with clean FAIL reports (no tracebacks); JS node --check OK; validate-skills
154 green; blocklist clean.

* fix(ascii-city-engine): address droid-review round 2 on PR #322

1. [P1] Engine: skip crossing props in the billboard loop so crosswalks
   render only as the documented ground band (no more floating '=' above
   each of the 168 crossings — a regression from the prior fix).
2. [P3] Engine: nearestStreet() filters to kind==='road' so the HUD 'On:'
   line names the street, not a named plaza/sidewalk ('Market Plaza' etc).
3. [P2] Validator: require each sign's text to be a recorded road name
   (collected from surfaces), enforcing the documented never-invented
   signage contract. A fabricated 'Made Up Avenue' sign now FAILs.
4. [P2][security] Validator: short-circuit the tile loop when
   MAX_FEATURES_PER_TILE is exceeded, and replace O(n^2) duplicate-id
   .count() scans with single-pass Counters, bounding the quadratic paths.

Verified: dense pack 1411/0 exit 0; fabricated-sign repro flags only the
injected sign and exits 1; broken/crash/dos/null packs all exit 1 with no
tracebacks; JS node --check OK; validate-skills 154 green; blocklist clean.

* fix(ascii-city-engine): address droid-review round 3 on PR #322

1. [P2] Validator: emit the signs rule unconditionally so a null/non-list
   'signs' value FAILs instead of passing silently (was gated on a truthy
   list check).
2. [P2] Validator: validate sign text against a pack-wide road-name set
   gathered across all tiles, so a sign in one tile may name a road whose
   surface lives in another (the documented multi-tile case).
3. [P2] Schema: require non-empty id/kind/text (minLength 1) on props and
   signs so the schema and validator agree on empty-string rejection.
4. [P3] Engine: drop dead signGrid/IX.key (the sign overlay iterates
   world.signs directly); cap edge-rasterization steps so a degenerate
   resolution (0) or pathologically long edge cannot spin unboundedly.
5. [P3] Validator: unknown prop kinds now pass with a reported fallback-'?'
   note instead of hard-failing, matching the documented fallback glyph and
   the engine's behavior.

Verified: dense pack 1411/0 exit 0; signs:null FAILs; fabricated sign FAILs;
multi-tile sign-to-road reference PASSes; broken/crash/dos/null all exit 1;
JS node --check OK; validate-skills 154 green; blocklist clean.

* fix(ascii-city-engine): address droid-review round 4 on PR #322

1. [P2] Engine: render props as once-per-frame perspective-projected one-cell
   billboards in an overlay pass (like signs) instead of during the ray march,
   eliminating the multi-row vertical streak a close prop produced. Verified in
   a live browser: signals/trees/crosswalks now render as discrete single cells.
2. [P3][security] Engine: bound aggregate rasterization in buildIndices() —
   cap surfaces (5000) and cells per surface (40000) so a crafted pack cannot
   freeze the tab on load (the validator's caps are not applied client-side).
3. [P2][security] Engine: guard sign text (missing/non-string text now skips
   the sign instead of throwing in the rAF loop and freezing the view).
4. [P3] Validator: still collect building/surface IDs for oversized tiles so
   pack-wide uniqueness detection runs even when the per-feature geometry checks
   are short-circuited (duplicates in an over-cap tile are no longer hidden).

Verified: dense pack 1411/0 exit 0; live browser render shows discrete props
(no streaks); broken/crash/dos/null/nullsign/fabric all exit 1, valid
multi-tile pack exit 0; JS node --check OK; validate-skills 154 green;
blocklist clean.

* fix(ascii-city-engine): address droid-review round 5 on PR #322

1. [P2][security] Engine: guard terrain() against non-finite x/y and guard the
   prop/sign overlay passes against non-array, non-object entries, so a
   malformed pack (missing y, signs=42, null entries) degrades gracefully
   instead of throwing in the rAF loop and freezing the view. Verified in a
   live browser: a pack with signs=42 + a prop missing y renders with the
   frame loop alive and no console errors beyond the favicon 404.
2. [P3] Engine: per-surface 'seen' set now dedupes cells across edges (was
   per-edge), eliminating the repeated linear includes() scan that made the
   rasterizer quadratic in the worst case.
3. [P3] Engine: raise the per-edge step cap to 20000 since the per-surface
   cell cap bounds total work, so long edges are fully sampled at the 2-5 m
   resolutions raleigh-poc.md recommends (fixes road-styling drops).
4. [P3] Validator: lower MAX_FEATURES_PER_TILE to 50,000 (shipped pack is
   1,385), bounding the quadratic pair tests more tightly.
5. [P3] raleigh-poc: correct walkthrough step 3 — East Hargett sign is ~141 m
   behind the spawn, not ahead; only North Wilmington is ahead. HUD count
   guards signs/props as arrays.

Verified: dense pack 1411/0 exit 0; broken/crash/dos/null/nullsign/fabric all
exit 1, valid multi-tile pack exit 0; malformed-pack live render survives;
JS node --check OK; validate-skills 154 green; blocklist clean.

* fix(ascii-city-engine): address droid-review round 6 on PR #322

1. [P1] Engine: props/signs overlay passes now use the corrected perpendicular
   distance (d*cos(ray_angle-heading)) for row projection, distance scaling,
   and the depth test — matching the ray march — so FOV-edge objects project to
   the right row and no longer falsely occlude or poison later depth tests.
2. [P2][security] Engine: terrain() guards malformed terrain metadata (missing
   terrain/resolution, non-positive resolution, missing origin, null elevations),
   so a crafted pack degrades to a clean error instead of freezing the tab.
   Verified live: a resolution-0/null-elevations pack shows 'Cannot load...'
   with no page errors.
3. [P3] raleigh-poc: walkthrough step 3 corrected — W/S only translate, so a
   167-deg-off sign needs A/D rotation, not 'hold S'.
4. [P3][security] Validator: all_points() guards buildings/surfaces/props/signs
   against truthy non-iterables (e.g. props=42), matching the other null guards,
   so malformed packs report structured FAIL instead of an uncaught TypeError.

Verified: dense pack 1411/0 exit 0; crash/dos/null/nullsign/fabric/props42/
broken all exit 1 (no tracebacks); valid multi-tile exit 0; malformed-terrain
live render shows clean error, no freeze; JS node --check OK; validate-skills
154 green; blocklist clean.

* fix(ascii-city-engine): address droid-review round 7 on PR #322

1. [P2] Engine: spatial-index buildings (footprint bbox -> grid cells) so the
   render loop and collision test find nearby buildings in O(nearby) instead of
   scanning the whole O(buildings) list per ray sample. Browser-measured frame
   cost dropped ~62ms (16 FPS) to 12.5ms mean (~80 FPS) on the dense pack.
2. [P2][security] Engine: terrain() guards null/ragged elevation rows, so a
   pack with a null row degrades to a clean error instead of freezing the tab
   (live-verified: null-row pack shows 'Cannot load...', no page errors).
3. [P2][security] Engine: cap sign text at 80 chars in the overlay pass, so a
   pathological pack-supplied sign cannot drive an unbounded per-frame loop.
4. [P3] Validator: reference FALLBACK_GLYPH constant (was dead) in the
   unknown-kinds report message.
5. [P3] raleigh-poc: fix stale expected validator tail (was 30/25; actual is
   159/899, rules_passed=1411).

Verified: dense pack 1411/0 exit 0; crash/dos/null/nullsign/fabric/props42/
broken all exit 1, valid multi-tile exit 0; null-row pack shows clean error,
no freeze; ~80 FPS browser-measured on dense pack; JS node --check OK;
validate-skills 154 green; blocklist clean.

* fix(ascii-city-engine): address droid-review round 8 on PR #322

1. [P1][security] Engine: bound the building spatial-index rasterization with
   MAX_BUILDING_PTS (2000) and MAX_BUILDING_CELLS (40000) and require >=3 finite
   footprint points, so a ~100-byte crafted footprint cannot drive a ~1e10-
   iteration synchronous hang on load (the surface rasterizer's cap, applied
   to the building index I added in round 7).
2. [P2][security] Engine: facingBuilding() filters footprints to finite points
   before reducing, so a building with a null element in its footprint no
   longer throws in the rAF loop on frame 1.
3. [P2][security] Engine: collides() and pointNearPolyline() filter footprints/
   polylines to valid array points before edge tests, so null footprint points
   no longer throw once the player enters those cells.
4. [P3] Validator: correct the feature-cap comment to 'buildings + surfaces +
   props + signs combined' (signs were already counted).

Verified: dense pack 1411/0 exit 0; ~85 FPS browser-measured (perf fix intact);
badfoot pack (null-point + missing-footprint buildings) renders with zero page
errors and frame loop alive; crash/dos/null/nullsign/fabric/props42/broken all
exit 1, valid multi-tile exit 0; JS node --check OK; validate-skills 154 green;
blocklist clean.
2026-08-16 05:51:25 -04:00

2.0 KiB

ASCII City Engine

Why Install This Skill

Turn public elevation, building, and street data into a portable, first-person colored-ASCII city without re-deriving coordinate, collision, and provenance rules. The skill keeps the renderer city-neutral and city packs interchangeable.

What You Get

  • A heightfield and solid-footprint engine architecture.
  • JSON Schemas for manifests and world tiles.
  • A deterministic offline city-pack validator.
  • A pure browser, single-file Canvas 2D reference engine.
  • A small redistributable downtown Raleigh sample with real public footprints.
  • Reproducible GIS ingestion and Raleigh walkthrough references.

Quick Start

python3 scripts/validate-city-pack.py assets/raleigh-downtown-sample
python3 -m http.server 8000

Then open http://localhost:8000/assets/ascii-city-engine.html. The scaffold reads assets/raleigh-downtown-sample/manifest.json for the spawn point and first world tile; use W/S or arrow keys to move and A/D or left/right arrows to turn. The sample is dense: named-street signs, traffic signals, crosswalks, trees, and transit stops all render from real recorded data, and the HUD names the street you are standing on and the building you are facing.

Triggers

Load this skill for requests to build a colored ASCII city renderer, create a terrain-following first-person walker, define a pluggable city pack, ingest public GIS into the provided contract, or reproduce the Raleigh proof of concept.

Do not load it for ordinary GIS analysis, WebGL/3D engines, multi-level interiors or tunnels, gameplay systems, or requests to commit full-resolution GIS archives.

Requirements

  • Python 3 standard library for validation and conversion examples.
  • A current desktop browser with Canvas 2D and fetch support.
  • Network access only while acquiring full-resolution public GIS data; validation and the committed sample run offline.
  • Source licenses that permit redistribution, with attribution and retrieval metadata.