ascii-city-engine
Build portable, first-person colored ASCII city engines and small GIS-derived city packs. Use when designing terrain-following walking, raycast character rendering, city-provider schemas, or reproducible public-GIS ingestion. Do not use for conventional 3D/WebGL games, multi-leve
Install
npx skills add https://github.com/magnus919/agent-skills/tree/main/ascii-city-engine
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
git clone https://github.com/magnus919/agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole magnus919/agent-skills collection as a plugin from our marketplace. Git is the plain clone.
README
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
fetchsupport. - 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.
Skill manifest
ASCII City Engine
Build a portable city experience in three layers: an engine, a city-provider contract, and a city pack. Keep city-specific facts out of engine code.
Workflow
- Define the pack boundary and local meter-based CRS.
- Acquire elevation, building, and road/path data from public sources.
- Verify downloads, licenses, units, and provenance before conversion.
- Reproject all geometry to local meters and emit the manifest plus world tiles.
- Validate the pack offline:
python3 scripts/validate-city-pack.py path/to/city-pack - Load the pack's first tile in
assets/ascii-city-engine.htmland test movement, grade following, and solid footprints. - Record data limitations and human acceptance evidence.
Required invariants
- Compute
feet_zfrom terrain andeye_zfrom feet plus eye height. - Reject null terrain, over-steep steps, and movement touching a solid footprint.
- Use deterministic building colors and clear missed rays every frame.
- Treat v1 as one ground height per
(x, y)column; reserve, but do not implement, a surface graph. - Keep the engine independent of any city, vendor, agent harness, or private infrastructure.
- Do not commit large or full-resolution source data. Commit only a small redistributable sample; document acquisition for the rest.
Reference routing
- Read references/engine-architecture.md for world math, rendering, collisions, and the scaffold contract.
- Read references/city-provider-contract.md when creating or validating a provider.
- Read references/gis-ingestion.md before downloading or converting GIS sources.
- Read references/raleigh-poc.md for the reproducible Raleigh proof of concept.
- Use templates/city-pack-manifest.schema.json and templates/world.schema.json as authoritative data contracts.
Available Scripts
This skill bundles one script; there are no others to discover.
| Script | Purpose | Invocation |
|---|---|---|
scripts/validate-city-pack.py |
Offline validator for a city pack: checks the manifest against the schema contract and verifies world tiles. Run it at workflow step 5, after emitting the manifest and tiles and before loading anything in the engine — do not proceed to in-engine testing until it passes. | python3 scripts/validate-city-pack.py path/to/city-pack |
Prerequisites
- Python 3 with standard library only; the validator requires no third-party packages.
- A city pack produced through the ingestion workflow: manifest plus world tiles reprojected to local meters (see references/gis-ingestion.md).
- The engine itself is a standalone HTML file (
assets/ascii-city-engine.html) that runs in any modern browser; no build step or server is required.
Limitations
- v1 supports one ground height per
(x, y)column: no interiors, bridges with traversable space below, tunnels, or multi-level geometry (a surface graph is reserved but not implemented). - The engine is software-rendered CPU canvas — no WebGL/GPU rendering or mobile controls.
- The skill does not include full-resolution GIS archives; packs are built reproducibly from public sources, committing only small redistributable samples.
- The validator is offline and structural: passing it does not substitute for human movement testing in the engine (workflow step 6).
Boundaries
Use a different skill or implementation approach for WebGL/GPU rendering, mobile controls, combat, interiors, bridges with traversable space below, tunnels, or general-purpose geospatial analysis. If asked to specialize this skill for one agent runtime, preserve the portable core and put runtime integration outside this skill. If asked to commit large GIS binaries, refuse and provide reproducible download/conversion commands instead.
Files (agent-skills)
-
assets
-
deliberately-broken-pack
-
manifest.json 280 B
{ "name": "broken-example", "version": "0.1.0", "crs": "local meters", "bounds": {"min_x": 10, "min_y": 0, "max_x": 0, "max_y": 10}, "tiles": [], "provenance": [{"name": "Example", "url": "https://example.org/data", "license": "Example", "retrieved": "not-a-date"}] } -
README.md 325 B
# Deliberately broken city pack This fixture is intentionally invalid. Its manifest has an empty tile list, an inverted x bound, and an invalid retrieval date. Run `python3 scripts/validate-city-pack.py assets/deliberately-broken-pack` from the skill directory to demonstrate deterministic `FAIL` output and a nonzero exit.
-
-
raleigh-downtown-sample
-
world
-
tile-0.json 497.3 KB
{"terrain":{"resolution_m":10.0,"origin":[0,0],"elevations":[[95.727,95.76,95.792,95.825,95.858,95.891,95.923,95.956,95.989,96.022,96.054,96.087,96.12,96.152,96.185,96.218,96.251,96.283,96.316,96.349,96.382,96.414,96.447,96.48,96.513,96.545,96.578,96.611,96.643,96.676,96.709,96.742,96.774,96.807,96.84,96.873,96.905,96.938,96.971,97.003,97.036,97.069,97.102,97.134,97.167,97.2,97.233,97.265,97.298,97.331,97.364,97.396,97.429,97.462,97.494,97.527,97.56,97.593,97.625,97.658,97.691,97.724,97.756,97.789],[95.803,95.836,95.869,95.902,95.935,95.968,96.001,96.033,96.066,96.099,96.132,96.165,96.198,96.231,96.264,96.297,96.33,96.363,96.396,96.428,96.461,96.494,96.527,96.56,96.593,96.626,96.659,96.692,96.725,96.758,96.791,96.824,96.856,96.889,96.922,96.955,96.988,97.021,97.054,97.087,97.12,97.153,97.186,97.219,97.251,97.284,97.317,97.35,97.383,97.416,97.449,97.482,97.515,97.548,97.581,97.614,97.647,97.679,97.712,97.745,97.778,97.811,97.844,97.877],[95.879,95.912,95.945,95.978,96.011,96.045,96.078,96.111,96.144,96.177,96.21,96.243,96.276,96.309,96.343,96.376,96.409,96.442,96.475,96.508,96.541,96.574,96.607,96.641,96.674,96.707,96.74,96.773,96.806,96.839,96.872,96.905,96.939,96.972,97.005,97.038,97.071,97.104,97.137,97.17,97.203,97.236,97.27,97.303,97.336,97.369,97.402,97.435,97.468,97.501,97.534,97.568,97.601,97.634,97.667,97.7,97.733,97.766,97.799,97.832,97.866,97.899,97.932,97.965],[95.955,95.988,96.022,96.055,96.088,96.121,96.155,96.188,96.221,96.255,96.288,96.321,96.355,96.388,96.421,96.454,96.488,96.521,96.554,96.588,96.621,96.654,96.688,96.721,96.754,96.787,96.821,96.854,96.887,96.921,96.954,96.987,97.021,97.054,97.087,97.12,97.154,97.187,97.22,97.254,97.287,97.32,97.354,97.387,97.42,97.453,97.487,97.52,97.553,97.587,97.62,97.653,97.687,97.72,97.753,97.786,97.82,97.853,97.886,97.92,97.953,97.986,98.02,98.053],[96.031,96.064,96.098,96.131,96.165,96.198,96.232,96.265,96.299,96.332,96.366,96.399,96.433,96.466,96.5,96.533,96.567,96.6,96.634,96.667,96.701,96.734,96.768,96.801,96.835,96.868,96.902,96.935,96.969,97.002,97.036,97.069,97.103,97.136,97.17,97.203,97.237,97.27,97.304,97.337,97.371,97.404,97.438,97.471,97.505,97.538,97.572,97.605,97.638,97.672,97.705,97.739,97.772,97.806,97.839,97.873,97.906,97.94,97.973,98.007,98.04,98.074,98.107,98.141],[96.107,96.141,96.174,96.208,96.242,96.275,96.309,96.343,96.376,96.41,96.444,96.477,96.511,96.545,96.578,96.612,96.646,96.68,96.713,96.747,96.781,96.814,96.848,96.882,96.915,96.949,96.983,97.016,97.05,97.084,97.117,97.151,97.185,97.218,97.252,97.286,97.319,97.353,97.387,97.42,97.454,97.488,97.522,97.555,97.589,97.623,97.656,97.69,97.724,97.757,97.791,97.825,97.858,97.892,97.926,97.959,97.993,98.027,98.06,98.094,98.128,98.161,98.195,98.229],[96.183,96.217,96.251,96.285,96.318,96.352,96.386,96.42,96.454,96.488,96.522,96.555,96.589,96.623,96.657,96.691,96.725,96.759,96.793,96.826,96.86,96.894,96.928,96.962,96.996,97.03,97.064,97.097,97.131,97.165,97.199,97.233,97.267,97.301,97.335,97.368,97.402,97.436,97.47,97.504,97.538,97.572,97.605,97.639,97.673,97.707,97.741,97.775,97.809,97.843,97.876,97.91,97.944,97.978,98.012,98.046,98.08,98.114,98.147,98.181,98.215,98.249,98.283,98.317],[96.259,96.293,96.327,96.361,96.395,96.429,96.463,96.497,96.531,96.565,96.6,96.634,96.668,96.702,96.736,96.77,96.804,96.838,96.872,96.906,96.94,96.974,97.008,97.042,97.076,97.11,97.144,97.179,97.213,97.247,97.281,97.315,97.349,97.383,97.417,97.451,97.485,97.519,97.553,97.587,97.621,97.655,97.689,97.724,97.758,97.792,97.826,97.86,97.894,97.928,97.962,97.996,98.03,98.064,98.098,98.132,98.166,98.2,98.234,98.268,98.303,98.337,98.371,98.405],[96.335,96.369,96.403,96.438,96.472,96.506,96.54,96.575,96.609,96.643,96.677,96.712,96.746,96.78,96.814,96.849,96.883,96.917,96.951,96.986,97.02,97.054,97.088,97.123,97.157,97.191,97.225,97.26,97.294,97.328,97.362,97.397,97.431,97.465,97.499,97.534,97.568,97.602,97.636,97.671,97.705,97.739,97.773,97.808,97.842,97.876,97.91,97.945,97.979,98.013,98.047,98.082,98.116,98.15,98.184,98.219,98.253,98.287,98.321,98.356,98.39,98.424,98.458,98.493],[96.411,96.445,96.48,96.514,96.549,96.583,96.618,96.652,96.686,96.721,96.755,96.79,96.824,96.859,96.893,96.927,96.962,96.996,97.031,97.065,97.1,97.134,97.169,97.203,97.237,97.272,97.306,97.341,97.375,97.41,97.444,97.479,97.513,97.547,97.582,97.616,97.651,97.685,97.72,97.754,97.789,97.823,97.857,97.892,97.926,97.961,97.995,98.03,98.064,98.098,98.133,98.167,98.202,98.236,98.271,98.305,98.34,98.374,98.408,98.443,98.477,98.512,98.546,98.581],[96.487,96.522,96.556,96.591,96.625,96.66,96.695,96.729,96.764,96.799,96.833,96.868,96.902,96.937,96.972,97.006,97.041,97.076,97.11,97.145,97.179,97.214,97.249,97.283,97.318,97.353,97.387,97.422,97.457,97.491,97.526,97.56,97.595,97.63,97.664,97.699,97.734,97.768,97.803,97.837,97.872,97.907,97.941,97.976,98.011,98.045,98.08,98.115,98.149,98.184,98.218,98.253,98.288,98.322,98.357,98.392,98.426,98.461,98.495,98.53,98.565,98.599,98.634,98.669],[96.563,96.598,96.632,96.667,96.702,96.737,96.772,96.807,96.841,96.876,96.911,96.946,96.981,97.016,97.05,97.085,97.12,97.155,97.19,97.224,97.259,97.294,97.329,97.364,97.399,97.433,97.468,97.503,97.538,97.573,97.607,97.642,97.677,97.712,97.747,97.782,97.816,97.851,97.886,97.921,97.956,97.991,98.025,98.06,98.095,98.13,98.165,98.199,98.234,98.269,98.304,98.339,98.374,98.408,98.443,98.478,98.513,98.548,98.582,98.617,98.652,98.687,98.722,98.757],[96.639,96.674,96.709,96.744,96.779,96.814,96.849,96.884,96.919,96.954,96.989,97.024,97.059,97.094,97.129,97.164,97.199,97.234,97.269,97.304,97.339,97.374,97.409,97.444,97.479,97.514,97.549,97.584,97.619,97.654,97.689,97.724,97.759,97.794,97.829,97.864,97.899,97.934,97.969,98.004,98.039,98.074,98.109,98.144,98.179,98.214,98.249,98.284,98.319,98.354,98.389,98.424,98.459,98.494,98.529,98.564,98.599,98.634,98.669,98.704,98.739,98.775,98.81,98.845],[96.715,96.75,96.785,96.82,96.856,96.891,96.926,96.961,96.996,97.032,97.067,97.102,97.137,97.172,97.208,97.243,97.278,97.313,97.348,97.384,97.419,97.454,97.489,97.524,97.56,97.595,97.63,97.665,97.7,97.736,97.771,97.806,97.841,97.876,97.912,97.947,97.982,98.017,98.052,98.088,98.123,98.158,98.193,98.228,98.264,98.299,98.334,98.369,98.404,98.44,98.475,98.51,98.545,98.58,98.616,98.651,98.686,98.721,98.756,98.792,98.827,98.862,98.897,98.932],[96.791,96.826,96.862,96.897,96.932,96.968,97.003,97.039,97.074,97.109,97.145,97.18,97.216,97.251,97.286,97.322,97.357,97.392,97.428,97.463,97.499,97.534,97.569,97.605,97.64,97.676,97.711,97.746,97.782,97.817,97.853,97.888,97.923,97.959,97.994,98.03,98.065,98.1,98.136,98.171,98.206,98.242,98.277,98.313,98.348,98.383,98.419,98.454,98.49,98.525,98.56,98.596,98.631,98.667,98.702,98.737,98.773,98.808,98.843,98.879,98.914,98.95,98.985,99.02],[96.867,96.902,96.938,96.974,97.009,97.045,97.08,97.116,97.151,97.187,97.223,97.258,97.294,97.329,97.365,97.401,97.436,97.472,97.507,97.543,97.578,97.614,97.65,97.685,97.721,97.756,97.792,97.827,97.863,97.899,97.934,97.97,98.005,98.041,98.077,98.112,98.148,98.183,98.219,98.254,98.29,98.326,98.361,98.397,98.432,98.468,98.504,98.539,98.575,98.61,98.646,98.681,98.717,98.753,98.788,98.824,98.859,98.895,98.931,98.966,99.002,99.037,99.073,99.108],[96.943,96.979,97.014,97.05,97.086,97.122,97.157,97.193,97.229,97.265,97.301,97.336,97.372,97.408,97.444,97.479,97.515,97.551,97.587,97.622,97.658,97.694,97.73,97.766,97.801,97.837,97.873,97.909,97.944,97.98,98.016,98.052,98.087,98.123,98.159,98.195,98.231,98.266,98.302,98.338,98.374,98.409,98.445,98.481,98.517,98.552,98.588,98.624,98.66,98.696,98.731,98.767,98.803,98.839,98.874,98.91,98.946,98.982,99.018,99.053,99.089,99.125,99.161,99.196],[97.019,97.055,97.091,97.127,97.163,97.199,97.235,97.271,97.306,97.342,97.378,97.414,97.45,97.486,97.522,97.558,97.594,97.63,97.666,97.702,97.738,97.774,97.81,97.846,97.882,97.918,97.954,97.99,98.026,98.062,98.098,98.134,98.17,98.206,98.241,98.277,98.313,98.349,98.385,98.421,98.457,98.493,98.529,98.565,98.601,98.637,98.673,98.709,98.745,98.781,98.817,98.853,98.889,98.925,98.961,98.997,99.033,99.069,99.105,99.14,99.176,99.212,99.248,99.284],[97.095,97.131,97.167,97.203,97.239,97.276,97.312,97.348,97.384,97.42,97.456,97.492,97.529,97.565,97.601,97.637,97.673,97.709,97.745,97.782,97.818,97.854,97.89,97.926,97.962,97.999,98.035,98.071,98.107,98.143,98.179,98.215,98.252,98.288,98.324,98.36,98.396,98.432,98.469,98.505,98.541,98.577,98.613,98.649,98.685,98.722,98.758,98.794,98.83,98.866,98.902,98.938,98.975,99.011,99.047,99.083,99.119,99.155,99.192,99.228,99.264,99.3,99.336,99.372],[97.171,97.207,97.243,97.28,97.316,97.352,97.389,97.425,97.461,97.498,97.534,97.571,97.607,97.643,97.68,97.716,97.752,97.789,97.825,97.861,97.898,97.934,97.97,98.007,98.043,98.079,98.116,98.152,98.188,98.225,98.261,98.297,98.334,98.37,98.406,98.443,98.479,98.515,98.552,98.588,98.624,98.661,98.697,98.733,98.77,98.806,98.842,98.879,98.915,98.951,98.988,99.024,99.061,99.097,99.133,99.17,99.206,99.242,99.279,99.315,99.351,99.388,99.424,99.46],[97.247,97.283,97.32,97.356,97.393,97.429,97.466,97.502,97.539,97.576,97.612,97.649,97.685,97.722,97.758,97.795,97.831,97.868,97.904,97.941,97.977,98.014,98.05,98.087,98.123,98.16,98.197,98.233,98.27,98.306,98.343,98.379,98.416,98.452,98.489,98.525,98.562,98.598,98.635,98.671,98.708,98.745,98.781,98.818,98.854,98.891,98.927,98.964,99.0,99.037,99.073,99.11,99.146,99.183,99.219,99.256,99.293,99.329,99.366,99.402,99.439,99.475,99.512,99.548],[97.323,97.359,97.396,97.433,97.47,97.506,97.543,97.58,97.616,97.653,97.69,97.727,97.763,97.8,97.837,97.874,97.91,97.947,97.984,98.02,98.057,98.094,98.131,98.167,98.204,98.241,98.277,98.314,98.351,98.388,98.424,98.461,98.498,98.535,98.571,98.608,98.645,98.681,98.718,98.755,98.792,98.828,98.865,98.902,98.938,98.975,99.012,99.049,99.085,99.122,99.159,99.196,99.232,99.269,99.306,99.342,99.379,99.416,99.453,99.489,99.526,99.563,99.599,99.636],[97.399,97.436,97.473,97.509,97.546,97.583,97.62,97.657,97.694,97.731,97.768,97.805,97.842,97.879,97.915,97.952,97.989,98.026,98.063,98.1,98.137,98.174,98.211,98.248,98.285,98.322,98.358,98.395,98.432,98.469,98.506,98.543,98.58,98.617,98.654,98.691,98.728,98.764,98.801,98.838,98.875,98.912,98.949,98.986,99.023,99.06,99.097,99.134,99.17,99.207,99.244,99.281,99.318,99.355,99.392,99.429,99.466,99.503,99.54,99.576,99.613,99.65,99.687,99.724],[97.475,97.512,97.549,97.586,97.623,97.66,97.697,97.734,97.772,97.809,97.846,97.883,97.92,97.957,97.994,98.031,98.068,98.105,98.143,98.18,98.217,98.254,98.291,98.328,98.365,98.402,98.439,98.476,98.514,98.551,98.588,98.625,98.662,98.699,98.736,98.773,98.81,98.847,98.885,98.922,98.959,98.996,99.033,99.07,99.107,99.144,99.181,99.218,99.256,99.293,99.33,99.367,99.404,99.441,99.478,99.515,99.552,99.589,99.627,99.664,99.701,99.738,99.775,99.812],[97.551,97.588,97.625,97.663,97.7,97.737,97.774,97.812,97.849,97.886,97.924,97.961,97.998,98.035,98.073,98.11,98.147,98.185,98.222,98.259,98.297,98.334,98.371,98.408,98.446,98.483,98.52,98.558,98.595,98.632,98.669,98.707,98.744,98.781,98.819,98.856,98.893,98.93,98.968,99.005,99.042,99.08,99.117,99.154,99.192,99.229,99.266,99.303,99.341,99.378,99.415,99.453,99.49,99.527,99.564,99.602,99.639,99.676,99.714,99.751,99.788,99.825,99.863,99.9],[97.627,97.664,97.702,97.739,97.777,97.814,97.852,97.889,97.927,97.964,98.001,98.039,98.076,98.114,98.151,98.189,98.226,98.264,98.301,98.339,98.376,98.414,98.451,98.489,98.526,98.564,98.601,98.639,98.676,98.714,98.751,98.789,98.826,98.864,98.901,98.939,98.976,99.014,99.051,99.088,99.126,99.163,99.201,99.238,99.276,99.313,99.351,99.388,99.426,99.463,99.501,99.538,99.576,99.613,99.651,99.688,99.726,99.763,99.801,99.838,99.876,99.913,99.951,99.988],[97.703,97.74,97.778,97.816,97.853,97.891,97.929,97.966,98.004,98.042,98.079,98.117,98.155,98.192,98.23,98.268,98.305,98.343,98.381,98.418,98.456,98.494,98.531,98.569,98.607,98.644,98.682,98.72,98.757,98.795,98.833,98.87,98.908,98.946,98.984,99.021,99.059,99.097,99.134,99.172,99.21,99.247,99.285,99.323,99.36,99.398,99.436,99.473,99.511,99.549,99.586,99.624,99.662,99.699,99.737,99.775,99.812,99.85,99.888,99.925,99.963,100.001,100.038,100.076],[97.779,97.817,97.854,97.892,97.93,97.968,98.006,98.044,98.082,98.119,98.157,98.195,98.233,98.271,98.309,98.347,98.384,98.422,98.46,98.498,98.536,98.574,98.612,98.649,98.687,98.725,98.763,98.801,98.839,98.877,98.915,98.952,98.99,99.028,99.066,99.104,99.142,99.18,99.217,99.255,99.293,99.331,99.369,99.407,99.445,99.482,99.52,99.558,99.596,99.634,99.672,99.71,99.747,99.785,99.823,99.861,99.899,99.937,99.975,100.013,100.05,100.088,100.126,100.164],[97.855,97.893,97.931,97.969,98.007,98.045,98.083,98.121,98.159,98.197,98.235,98.273,98.311,98.349,98.387,98.425,98.463,98.502,98.54,98.578,98.616,98.654,98.692,98.73,98.768,98.806,98.844,98.882,98.92,98.958,98.996,99.034,99.072,99.11,99.148,99.186,99.225,99.263,99.301,99.339,99.377,99.415,99.453,99.491,99.529,99.567,99.605,99.643,99.681,99.719,99.757,99.795,99.833,99.871,99.909,99.947,99.986,100.024,100.062,100.1,100.138,100.176,100.214,100.252],[97.931,97.969,98.007,98.045,98.084,98.122,98.16,98.198,98.237,98.275,98.313,98.351,98.39,98.428,98.466,98.504,98.542,98.581,98.619,98.657,98.695,98.734,98.772,98.81,98.848,98.887,98.925,98.963,99.001,99.04,99.078,99.116,99.154,99.193,99.231,99.269,99.307,99.346,99.384,99.422,99.46,99.499,99.537,99.575,99.613,99.652,99.69,99.728,99.766,99.804,99.843,99.881,99.919,99.957,99.996,100.034,100.072,100.11,100.149,100.187,100.225,100.263,100.302,100.34],[98.007,98.045,98.083,98.122,98.16,98.199,98.237,98.276,98.314,98.352,98.391,98.429,98.468,98.506,98.545,98.583,98.622,98.66,98.698,98.737,98.775,98.814,98.852,98.891,98.929,98.967,99.006,99.044,99.083,99.121,99.16,99.198,99.236,99.275,99.313,99.352,99.39,99.429,99.467,99.505,99.544,99.582,99.621,99.659,99.698,99.736,99.774,99.813,99.851,99.89,99.928,99.967,100.005,100.044,100.082,100.12,100.159,100.197,100.236,100.274,100.313,100.351,100.389,100.428],[98.083,98.121,98.16,98.198,98.237,98.276,98.314,98.353,98.392,98.43,98.469,98.507,98.546,98.585,98.623,98.662,98.701,98.739,98.778,98.816,98.855,98.894,98.932,98.971,99.01,99.048,99.087,99.125,99.164,99.203,99.241,99.28,99.319,99.357,99.396,99.434,99.473,99.512,99.55,99.589,99.627,99.666,99.705,99.743,99.782,99.821,99.859,99.898,99.936,99.975,100.014,100.052,100.091,100.13,100.168,100.207,100.245,100.284,100.323,100.361,100.4,100.439,100.477,100.516],[98.159,98.197,98.236,98.275,98.314,98.353,98.391,98.43,98.469,98.508,98.547,98.586,98.624,98.663,98.702,98.741,98.78,98.818,98.857,98.896,98.935,98.974,99.012,99.051,99.09,99.129,99.168,99.207,99.245,99.284,99.323,99.362,99.401,99.439,99.478,99.517,99.556,99.595,99.633,99.672,99.711,99.75,99.789,99.828,99.866,99.905,99.944,99.983,100.022,100.06,100.099,100.138,100.177,100.216,100.254,100.293,100.332,100.371,100.41,100.449,100.487,100.526,100.565,100.604],[98.235,98.274,98.313,98.352,98.391,98.43,98.469,98.508,98.547,98.586,98.625,98.664,98.703,98.742,98.781,98.82,98.859,98.898,98.937,98.976,99.015,99.054,99.093,99.132,99.171,99.21,99.249,99.288,99.327,99.366,99.405,99.444,99.483,99.522,99.561,99.6,99.639,99.678,99.717,99.756,99.795,99.834,99.873,99.912,99.951,99.99,100.029,100.068,100.107,100.146,100.185,100.224,100.263,100.302,100.341,100.38,100.419,100.458,100.497,100.536,100.575,100.614,100.653,100.692],[98.311,98.35,98.389,98.428,98.467,98.507,98.546,98.585,98.624,98.663,98.702,98.742,98.781,98.82,98.859,98.898,98.938,98.977,99.016,99.055,99.094,99.134,99.173,99.212,99.251,99.29,99.33,99.369,99.408,99.447,99.486,99.526,99.565,99.604,99.643,99.682,99.721,99.761,99.8,99.839,99.878,99.917,99.957,99.996,100.035,100.074,100.113,100.153,100.192,100.231,100.27,100.309,100.349,100.388,100.427,100.466,100.505,100.545,100.584,100.623,100.662,100.701,100.74,100.78],[98.387,98.426,98.465,98.505,98.544,98.583,98.623,98.662,98.702,98.741,98.78,98.82,98.859,98.899,98.938,98.977,99.017,99.056,99.095,99.135,99.174,99.214,99.253,99.292,99.332,99.371,99.41,99.45,99.489,99.529,99.568,99.607,99.647,99.686,99.726,99.765,99.804,99.844,99.883,99.922,99.962,100.001,100.041,100.08,100.119,100.159,100.198,100.238,100.277,100.316,100.356,100.395,100.434,100.474,100.513,100.553,100.592,100.631,100.671,100.71,100.749,100.789,100.828,100.868],[98.463,98.502,98.542,98.581,98.621,98.66,98.7,98.74,98.779,98.819,98.858,98.898,98.937,98.977,99.017,99.056,99.096,99.135,99.175,99.214,99.254,99.294,99.333,99.373,99.412,99.452,99.491,99.531,99.571,99.61,99.65,99.689,99.729,99.768,99.808,99.848,99.887,99.927,99.966,100.006,100.045,100.085,100.125,100.164,100.204,100.243,100.283,100.322,100.362,100.402,100.441,100.481,100.52,100.56,100.599,100.639,100.679,100.718,100.758,100.797,100.837,100.876,100.916,100.956],[98.539,98.578,98.618,98.658,98.698,98.737,98.777,98.817,98.857,98.896,98.936,98.976,99.016,99.055,99.095,99.135,99.175,99.214,99.254,99.294,99.334,99.374,99.413,99.453,99.493,99.533,99.572,99.612,99.652,99.692,99.731,99.771,99.811,99.851,99.89,99.93,99.97,100.01,100.049,100.089,100.129,100.169,100.209,100.248,100.288,100.328,100.368,100.407,100.447,100.487,100.527,100.566,100.606,100.646,100.686,100.725,100.765,100.805,100.845,100.885,100.924,100.964,101.004,101.044],[98.615,98.654,98.694,98.734,98.774,98.814,98.854,98.894,98.934,98.974,99.014,99.054,99.094,99.134,99.174,99.214,99.254,99.294,99.334,99.374,99.414,99.454,99.493,99.533,99.573,99.613,99.653,99.693,99.733,99.773,99.813,99.853,99.893,99.933,99.973,100.013,100.053,100.093,100.133,100.173,100.213,100.253,100.293,100.332,100.372,100.412,100.452,100.492,100.532,100.572,100.612,100.652,100.692,100.732,100.772,100.812,100.852,100.892,100.932,100.972,101.012,101.052,101.092,101.132],[98.69,98.731,98.771,98.811,98.851,98.891,98.931,98.971,99.012,99.052,99.092,99.132,99.172,99.212,99.252,99.293,99.333,99.373,99.413,99.453,99.493,99.533,99.574,99.614,99.654,99.694,99.734,99.774,99.814,99.855,99.895,99.935,99.975,100.015,100.055,100.095,100.136,100.176,100.216,100.256,100.296,100.336,100.376,100.417,100.457,100.497,100.537,100.577,100.617,100.657,100.698,100.738,100.778,100.818,100.858,100.898,100.938,100.979,101.019,101.059,101.099,101.139,101.179,101.219],[98.766,98.807,98.847,98.887,98.928,98.968,99.008,99.049,99.089,99.129,99.17,99.21,99.25,99.291,99.331,99.371,99.412,99.452,99.492,99.533,99.573,99.613,99.654,99.694,99.734,99.775,99.815,99.855,99.896,99.936,99.976,100.017,100.057,100.097,100.138,100.178,100.218,100.259,100.299,100.339,100.38,100.42,100.46,100.501,100.541,100.581,100.622,100.662,100.702,100.743,100.783,100.823,100.864,100.904,100.944,100.985,101.025,101.065,101.106,101.146,101.186,101.227,101.267,101.307],[98.842,98.883,98.924,98.964,99.005,99.045,99.086,99.126,99.167,99.207,99.248,99.288,99.329,99.369,99.41,99.45,99.491,99.531,99.572,99.612,99.653,99.693,99.734,99.774,99.815,99.856,99.896,99.937,99.977,100.018,100.058,100.099,100.139,100.18,100.22,100.261,100.301,100.342,100.382,100.423,100.463,100.504,100.544,100.585,100.625,100.666,100.707,100.747,100.788,100.828,100.869,100.909,100.95,100.99,101.031,101.071,101.112,101.152,101.193,101.233,101.274,101.314,101.355,101.395],[98.918,98.959,99.0,99.041,99.081,99.122,99.163,99.203,99.244,99.285,99.326,99.366,99.407,99.448,99.488,99.529,99.57,99.611,99.651,99.692,99.733,99.773,99.814,99.855,99.896,99.936,99.977,100.018,100.058,100.099,100.14,100.181,100.221,100.262,100.303,100.343,100.384,100.425,100.466,100.506,100.547,100.588,100.628,100.669,100.71,100.751,100.791,100.832,100.873,100.913,100.954,100.995,101.036,101.076,101.117,101.158,101.198,101.239,101.28,101.321,101.361,101.402,101.443,101.483],[98.994,99.035,99.076,99.117,99.158,99.199,99.24,99.281,99.322,99.363,99.403,99.444,99.485,99.526,99.567,99.608,99.649,99.69,99.731,99.772,99.812,99.853,99.894,99.935,99.976,100.017,100.058,100.099,100.14,100.181,100.222,100.262,100.303,100.344,100.385,100.426,100.467,100.508,100.549,100.59,100.631,100.671,100.712,100.753,100.794,100.835,100.876,100.917,100.958,100.999,101.04,101.08,101.121,101.162,101.203,101.244,101.285,101.326,101.367,101.408,101.449,101.49,101.53,101.571],[99.07,99.112,99.153,99.194,99.235,99.276,99.317,99.358,99.399,99.44,99.481,99.522,99.564,99.605,99.646,99.687,99.728,99.769,99.81,99.851,99.892,99.933,99.974,100.016,100.057,100.098,100.139,100.18,100.221,100.262,100.303,100.344,100.385,100.426,100.468,100.509,100.55,100.591,100.632,100.673,100.714,100.755,100.796,100.837,100.879,100.92,100.961,101.002,101.043,101.084,101.125,101.166,101.207,101.248,101.289,101.331,101.372,101.413,101.454,101.495,101.536,101.577,101.618,101.659],[99.146,99.188,99.229,99.27,99.312,99.353,99.394,99.435,99.477,99.518,99.559,99.601,99.642,99.683,99.724,99.766,99.807,99.848,99.89,99.931,99.972,100.013,100.055,100.096,100.137,100.178,100.22,100.261,100.302,100.344,100.385,100.426,100.467,100.509,100.55,100.591,100.633,100.674,100.715,100.756,100.798,100.839,100.88,100.922,100.963,101.004,101.045,101.087,101.128,101.169,101.211,101.252,101.293,101.334,101.376,101.417,101.458,101.5,101.541,101.582,101.623,101.665,101.706,101.747],[99.222,99.264,99.305,99.347,99.388,99.43,99.471,99.513,99.554,99.596,99.637,99.679,99.72,99.762,99.803,99.844,99.886,99.927,99.969,100.01,100.052,100.093,100.135,100.176,100.218,100.259,100.301,100.342,100.384,100.425,100.467,100.508,100.55,100.591,100.632,100.674,100.715,100.757,100.798,100.84,100.881,100.923,100.964,101.006,101.047,101.089,101.13,101.172,101.213,101.255,101.296,101.338,101.379,101.42,101.462,101.503,101.545,101.586,101.628,101.669,101.711,101.752,101.794,101.835],[99.298,99.34,99.382,99.423,99.465,99.507,99.548,99.59,99.632,99.673,99.715,99.757,99.798,99.84,99.882,99.923,99.965,100.007,100.048,100.09,100.132,100.173,100.215,100.257,100.298,100.34,100.382,100.423,100.465,100.507,100.548,100.59,100.632,100.673,100.715,100.757,100.798,100.84,100.882,100.923,100.965,101.007,101.048,101.09,101.132,101.173,101.215,101.257,101.298,101.34,101.382,101.423,101.465,101.507,101.548,101.59,101.632,101.673,101.715,101.757,101.798,101.84,101.882,101.923],[99.374,99.416,99.458,99.5,99.542,99.584,99.625,99.667,99.709,99.751,99.793,99.835,99.877,99.918,99.96,100.002,100.044,100.086,100.128,100.17,100.211,100.253,100.295,100.337,100.379,100.421,100.463,100.504,100.546,100.588,100.63,100.672,100.714,100.756,100.797,100.839,100.881,100.923,100.965,101.007,101.049,101.09,101.132,101.174,101.216,101.258,101.3,101.341,101.383,101.425,101.467,101.509,101.551,101.593,101.634,101.676,101.718,101.76,101.802,101.844,101.886,101.927,101.969,102.011],[99.45,99.492,99.534,99.576,99.619,99.661,99.703,99.745,99.787,99.829,99.871,99.913,99.955,99.997,100.039,100.081,100.123,100.165,100.207,100.249,100.291,100.333,100.375,100.417,100.459,100.501,100.543,100.586,100.628,100.67,100.712,100.754,100.796,100.838,100.88,100.922,100.964,101.006,101.048,101.09,101.132,101.174,101.216,101.258,101.3,101.342,101.384,101.426,101.468,101.51,101.553,101.595,101.637,101.679,101.721,101.763,101.805,101.847,101.889,101.931,101.973,102.015,102.057,102.099],[99.526,99.569,99.611,99.653,99.695,99.738,99.78,99.822,99.864,99.906,99.949,99.991,100.033,100.075,100.118,100.16,100.202,100.244,100.287,100.329,100.371,100.413,100.455,100.498,100.54,100.582,100.624,100.667,100.709,100.751,100.793,100.836,100.878,100.92,100.962,101.005,101.047,101.089,101.131,101.173,101.216,101.258,101.3,101.342,101.385,101.427,101.469,101.511,101.554,101.596,101.638,101.68,101.722,101.765,101.807,101.849,101.891,101.934,101.976,102.018,102.06,102.103,102.145,102.187],[99.602,99.645,99.687,99.73,99.772,99.814,99.857,99.899,99.942,99.984,100.027,100.069,100.111,100.154,100.196,100.239,100.281,100.324,100.366,100.408,100.451,100.493,100.536,100.578,100.62,100.663,100.705,100.748,100.79,100.833,100.875,100.917,100.96,101.002,101.045,101.087,101.13,101.172,101.214,101.257,101.299,101.342,101.384,101.427,101.469,101.511,101.554,101.596,101.639,101.681,101.724,101.766,101.808,101.851,101.893,101.936,101.978,102.02,102.063,102.105,102.148,102.19,102.233,102.275],[99.678,99.721,99.764,99.806,99.849,99.891,99.934,99.977,100.019,100.062,100.104,100.147,100.19,100.232,100.275,100.318,100.36,100.403,100.445,100.488,100.531,100.573,100.616,100.658,100.701,100.744,100.786,100.829,100.872,100.914,100.957,100.999,101.042,101.085,101.127,101.17,101.212,101.255,101.298,101.34,101.383,101.425,101.468,101.511,101.553,101.596,101.639,101.681,101.724,101.766,101.809,101.852,101.894,101.937,101.979,102.022,102.065,102.107,102.15,102.193,102.235,102.278,102.32,102.363],[99.754,99.797,99.84,99.883,99.926,99.968,100.011,100.054,100.097,100.14,100.182,100.225,100.268,100.311,100.354,100.396,100.439,100.482,100.525,100.568,100.61,100.653,100.696,100.739,100.782,100.824,100.867,100.91,100.953,100.996,101.038,101.081,101.124,101.167,101.21,101.252,101.295,101.338,101.381,101.424,101.466,101.509,101.552,101.595,101.638,101.68,101.723,101.766,101.809,101.852,101.894,101.937,101.98,102.023,102.066,102.109,102.151,102.194,102.237,102.28,102.323,102.365,102.408,102.451],[99.83,99.873,99.916,99.959,100.002,100.045,100.088,100.131,100.174,100.217,100.26,100.303,100.346,100.389,100.432,100.475,100.518,100.561,100.604,100.647,100.69,100.733,100.776,100.819,100.862,100.905,100.948,100.991,101.034,101.077,101.12,101.163,101.206,101.249,101.292,101.335,101.378,101.421,101.464,101.507,101.55,101.593,101.636,101.679,101.722,101.765,101.808,101.851,101.894,101.937,101.98,102.023,102.066,102.109,102.152,102.195,102.238,102.281,102.324,102.367,102.41,102.453,102.496,102.539],[99.906,99.949,99.993,100.036,100.079,100.122,100.165,100.209,100.252,100.295,100.338,100.381,100.424,100.468,100.511,100.554,100.597,100.64,100.684,100.727,100.77,100.813,100.856,100.9,100.943,100.986,101.029,101.072,101.115,101.159,101.202,101.245,101.288,101.331,101.375,101.418,101.461,101.504,101.547,101.59,101.634,101.677,101.72,101.763,101.806,101.85,101.893,101.936,101.979,102.022,102.065,102.109,102.152,102.195,102.238,102.281,102.325,102.368,102.411,102.454,102.497,102.54,102.584,102.627],[99.982,100.026,100.069,100.112,100.156,100.199,100.243,100.286,100.329,100.373,100.416,100.459,100.503,100.546,100.59,100.633,100.676,100.72,100.763,100.806,100.85,100.893,100.936,100.98,101.023,101.067,101.11,101.153,101.197,101.24,101.283,101.327,101.37,101.414,101.457,101.5,101.544,101.587,101.63,101.674,101.717,101.761,101.804,101.847,101.891,101.934,101.977,102.021,102.064,102.108,102.151,102.194,102.238,102.281,102.324,102.368,102.411,102.455,102.498,102.541,102.585,102.628,102.671,102.715],[100.058,100.102,100.145,100.189,100.233,100.276,100.32,100.363,100.407,100.45,100.494,100.537,100.581,100.625,100.668,100.712,100.755,100.799,100.842,100.886,100.93,100.973,101.017,101.06,101.104,101.147,101.191,101.234,101.278,101.322,101.365,101.409,101.452,101.496,101.539,101.583,101.627,101.67,101.714,101.757,101.801,101.844,101.888,101.932,101.975,102.019,102.062,102.106,102.149,102.193,102.236,102.28,102.324,102.367,102.411,102.454,102.498,102.541,102.585,102.629,102.672,102.716,102.759,102.803],[100.134,100.178,100.222,100.266,100.309,100.353,100.397,100.441,100.484,100.528,100.572,100.616,100.659,100.703,100.747,100.791,100.834,100.878,100.922,100.966,101.009,101.053,101.097,101.141,101.184,101.228,101.272,101.316,101.359,101.403,101.447,101.491,101.534,101.578,101.622,101.666,101.709,101.753,101.797,101.841,101.884,101.928,101.972,102.016,102.059,102.103,102.147,102.191,102.234,102.278,102.322,102.366,102.409,102.453,102.497,102.541,102.584,102.628,102.672,102.716,102.759,102.803,102.847,102.891],[100.21,100.254,100.298,100.342,100.386,100.43,100.474,100.518,100.562,100.606,100.65,100.694,100.738,100.782,100.825,100.869,100.913,100.957,101.001,101.045,101.089,101.133,101.177,101.221,101.265,101.309,101.353,101.397,101.441,101.485,101.529,101.572,101.616,101.66,101.704,101.748,101.792,101.836,101.88,101.924,101.968,102.012,102.056,102.1,102.144,102.188,102.232,102.276,102.32,102.363,102.407,102.451,102.495,102.539,102.583,102.627,102.671,102.715,102.759,102.803,102.847,102.891,102.935,102.979],[100.286,100.33,100.374,100.419,100.463,100.507,100.551,100.595,100.639,100.683,100.728,100.772,100.816,100.86,100.904,100.948,100.992,101.036,101.081,101.125,101.169,101.213,101.257,101.301,101.345,101.39,101.434,101.478,101.522,101.566,101.61,101.654,101.699,101.743,101.787,101.831,101.875,101.919,101.963,102.007,102.052,102.096,102.14,102.184,102.228,102.272,102.316,102.361,102.405,102.449,102.493,102.537,102.581,102.625,102.669,102.714,102.758,102.802,102.846,102.89,102.934,102.978,103.023,103.067],[100.362,100.407,100.451,100.495,100.54,100.584,100.628,100.672,100.717,100.761,100.805,100.85,100.894,100.938,100.983,101.027,101.071,101.116,101.16,101.204,101.249,101.293,101.337,101.382,101.426,101.47,101.515,101.559,101.603,101.648,101.692,101.736,101.781,101.825,101.869,101.914,101.958,102.002,102.047,102.091,102.135,102.179,102.224,102.268,102.312,102.357,102.401,102.445,102.49,102.534,102.578,102.623,102.667,102.711,102.756,102.8,102.844,102.889,102.933,102.977,103.022,103.066,103.11,103.155],[100.438,100.483,100.527,100.572,100.616,100.661,100.705,100.75,100.794,100.839,100.883,100.928,100.972,101.017,101.061,101.106,101.15,101.195,101.239,101.284,101.328,101.373,101.418,101.462,101.507,101.551,101.596,101.64,101.685,101.729,101.774,101.818,101.863,101.907,101.952,101.996,102.041,102.085,102.13,102.174,102.219,102.263,102.308,102.352,102.397,102.441,102.486,102.53,102.575,102.619,102.664,102.708,102.753,102.797,102.842,102.886,102.931,102.975,103.02,103.065,103.109,103.154,103.198,103.243],[100.514,100.559,100.604,100.648,100.693,100.738,100.782,100.827,100.872,100.917,100.961,101.006,101.051,101.095,101.14,101.185,101.229,101.274,101.319,101.364,101.408,101.453,101.498,101.542,101.587,101.632,101.676,101.721,101.766,101.811,101.855,101.9,101.945,101.989,102.034,102.079,102.124,102.168,102.213,102.258,102.302,102.347,102.392,102.436,102.481,102.526,102.571,102.615,102.66,102.705,102.749,102.794,102.839,102.884,102.928,102.973,103.018,103.062,103.107,103.152,103.196,103.241,103.286,103.331],[100.59,100.635,100.68,100.725,100.77,100.815,100.86,100.904,100.949,100.994,101.039,101.084,101.129,101.174,101.219,101.264,101.308,101.353,101.398,101.443,101.488,101.533,101.578,101.623,101.668,101.713,101.757,101.802,101.847,101.892,101.937,101.982,102.027,102.072,102.117,102.161,102.206,102.251,102.296,102.341,102.386,102.431,102.476,102.521,102.566,102.61,102.655,102.7,102.745,102.79,102.835,102.88,102.925,102.97,103.014,103.059,103.104,103.149,103.194,103.239,103.284,103.329,103.374,103.419],[100.666,100.711,100.756,100.801,100.846,100.892,100.937,100.982,101.027,101.072,101.117,101.162,101.207,101.252,101.297,101.342,101.388,101.433,101.478,101.523,101.568,101.613,101.658,101.703,101.748,101.793,101.838,101.883,101.929,101.974,102.019,102.064,102.109,102.154,102.199,102.244,102.289,102.334,102.379,102.424,102.47,102.515,102.56,102.605,102.65,102.695,102.74,102.785,102.83,102.875,102.92,102.965,103.011,103.056,103.101,103.146,103.191,103.236,103.281,103.326,103.371,103.416,103.461,103.506],[100.742,100.787,100.833,100.878,100.923,100.969,101.014,101.059,101.104,101.15,101.195,101.24,101.285,101.331,101.376,101.421,101.467,101.512,101.557,101.602,101.648,101.693,101.738,101.783,101.829,101.874,101.919,101.965,102.01,102.055,102.1,102.146,102.191,102.236,102.281,102.327,102.372,102.417,102.463,102.508,102.553,102.598,102.644,102.689,102.734,102.779,102.825,102.87,102.915,102.961,103.006,103.051,103.096,103.142,103.187,103.232,103.278,103.323,103.368,103.413,103.459,103.504,103.549,103.594],[100.818,100.864,100.909,100.955,101.0,101.045,101.091,101.136,101.182,101.227,101.273,101.318,101.364,101.409,101.455,101.5,101.546,101.591,101.636,101.682,101.727,101.773,101.818,101.864,101.909,101.955,102.0,102.046,102.091,102.137,102.182,102.228,102.273,102.318,102.364,102.409,102.455,102.5,102.546,102.591,102.637,102.682,102.728,102.773,102.819,102.864,102.909,102.955,103.0,103.046,103.091,103.137,103.182,103.228,103.273,103.319,103.364,103.41,103.455,103.501,103.546,103.591,103.637,103.682],[100.894,100.94,100.985,101.031,101.077,101.122,101.168,101.214,101.259,101.305,101.351,101.396,101.442,101.488,101.533,101.579,101.625,101.67,101.716,101.762,101.807,101.853,101.899,101.944,101.99,102.035,102.081,102.127,102.172,102.218,102.264,102.309,102.355,102.401,102.446,102.492,102.538,102.583,102.629,102.675,102.72,102.766,102.812,102.857,102.903,102.949,102.994,103.04,103.086,103.131,103.177,103.222,103.268,103.314,103.359,103.405,103.451,103.496,103.542,103.588,103.633,103.679,103.725,103.77],[100.97,101.016,101.062,101.108,101.153,101.199,101.245,101.291,101.337,101.383,101.429,101.474,101.52,101.566,101.612,101.658,101.704,101.749,101.795,101.841,101.887,101.933,101.979,102.025,102.07,102.116,102.162,102.208,102.254,102.3,102.345,102.391,102.437,102.483,102.529,102.575,102.621,102.666,102.712,102.758,102.804,102.85,102.896,102.941,102.987,103.033,103.079,103.125,103.171,103.216,103.262,103.308,103.354,103.4,103.446,103.492,103.537,103.583,103.629,103.675,103.721,103.767,103.812,103.858],[101.046,101.092,101.138,101.184,101.23,101.276,101.322,101.368,101.414,101.46,101.506,101.552,101.599,101.645,101.691,101.737,101.783,101.829,101.875,101.921,101.967,102.013,102.059,102.105,102.151,102.197,102.243,102.289,102.335,102.381,102.427,102.473,102.519,102.565,102.611,102.657,102.703,102.749,102.795,102.841,102.887,102.934,102.98,103.026,103.072,103.118,103.164,103.21,103.256,103.302,103.348,103.394,103.44,103.486,103.532,103.578,103.624,103.67,103.716,103.762,103.808,103.854,103.9,103.946],[101.122,101.168,101.215,101.261,101.307,101.353,101.399,101.446,101.492,101.538,101.584,101.631,101.677,101.723,101.769,101.815,101.862,101.908,101.954,102.0,102.047,102.093,102.139,102.185,102.231,102.278,102.324,102.37,102.416,102.463,102.509,102.555,102.601,102.647,102.694,102.74,102.786,102.832,102.879,102.925,102.971,103.017,103.064,103.11,103.156,103.202,103.248,103.295,103.341,103.387,103.433,103.48,103.526,103.572,103.618,103.664,103.711,103.757,103.803,103.849,103.896,103.942,103.988,104.034],[101.198,101.244,101.291,101.337,101.384,101.43,101.477,101.523,101.569,101.616,101.662,101.709,101.755,101.801,101.848,101.894,101.941,101.987,102.034,102.08,102.126,102.173,102.219,102.266,102.312,102.358,102.405,102.451,102.498,102.544,102.591,102.637,102.683,102.73,102.776,102.823,102.869,102.915,102.962,103.008,103.055,103.101,103.147,103.194,103.24,103.287,103.333,103.38,103.426,103.472,103.519,103.565,103.612,103.658,103.704,103.751,103.797,103.844,103.89,103.937,103.983,104.029,104.076,104.122],[101.274,101.321,101.367,101.414,101.46,101.507,101.554,101.6,101.647,101.693,101.74,101.787,101.833,101.88,101.927,101.973,102.02,102.066,102.113,102.16,102.206,102.253,102.299,102.346,102.393,102.439,102.486,102.532,102.579,102.626,102.672,102.719,102.765,102.812,102.859,102.905,102.952,102.998,103.045,103.092,103.138,103.185,103.231,103.278,103.325,103.371,103.418,103.464,103.511,103.558,103.604,103.651,103.698,103.744,103.791,103.837,103.884,103.931,103.977,104.024,104.07,104.117,104.164,104.21],[101.35,101.397,101.444,101.49,101.537,101.584,101.631,101.678,101.724,101.771,101.818,101.865,101.912,101.958,102.005,102.052,102.099,102.146,102.192,102.239,102.286,102.333,102.38,102.426,102.473,102.52,102.567,102.614,102.66,102.707,102.754,102.801,102.847,102.894,102.941,102.988,103.035,103.081,103.128,103.175,103.222,103.269,103.315,103.362,103.409,103.456,103.503,103.549,103.596,103.643,103.69,103.737,103.783,103.83,103.877,103.924,103.971,104.017,104.064,104.111,104.158,104.205,104.251,104.298],[101.426,101.473,101.52,101.567,101.614,101.661,101.708,101.755,101.802,101.849,101.896,101.943,101.99,102.037,102.084,102.131,102.178,102.225,102.272,102.319,102.366,102.413,102.46,102.507,102.554,102.601,102.648,102.695,102.742,102.789,102.836,102.883,102.93,102.977,103.024,103.07,103.117,103.164,103.211,103.258,103.305,103.352,103.399,103.446,103.493,103.54,103.587,103.634,103.681,103.728,103.775,103.822,103.869,103.916,103.963,104.01,104.057,104.104,104.151,104.198,104.245,104.292,104.339,104.386],[101.502,101.549,101.596,101.644,101.691,101.738,101.785,101.832,101.879,101.927,101.974,102.021,102.068,102.115,102.162,102.21,102.257,102.304,102.351,102.398,102.446,102.493,102.54,102.587,102.634,102.681,102.729,102.776,102.823,102.87,102.917,102.964,103.012,103.059,103.106,103.153,103.2,103.247,103.295,103.342,103.389,103.436,103.483,103.531,103.578,103.625,103.672,103.719,103.766,103.814,103.861,103.908,103.955,104.002,104.049,104.097,104.144,104.191,104.238,104.285,104.333,104.38,104.427,104.474],[101.578,101.625,101.673,101.72,101.767,101.815,101.862,101.91,101.957,102.004,102.052,102.099,102.146,102.194,102.241,102.288,102.336,102.383,102.431,102.478,102.525,102.573,102.62,102.667,102.715,102.762,102.809,102.857,102.904,102.952,102.999,103.046,103.094,103.141,103.188,103.236,103.283,103.331,103.378,103.425,103.473,103.52,103.567,103.615,103.662,103.709,103.757,103.804,103.852,103.899,103.946,103.994,104.041,104.088,104.136,104.183,104.23,104.278,104.325,104.373,104.42,104.467,104.515,104.562]],"provenance":{"source":"USGS 3DEP Elevation Point Query Service","url":"https://epqs.nationalmap.gov/v1/json","license":"US public domain","retrieved":"2026-08-15","confidence":0.72,"method":"Four corner observations bilinearly interpolated"}},"buildings":[{"id":"osm-way-47662234","footprint":[[139.56,0.0],[137.3,0.0],[137.68,2.07],[139.91,1.98]],"base_elev_m":96.19,"height_m":3.2,"color":"#7f4eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true},{"id":"osm-way-47663082","footprint":[[161.46,0.0],[162.61,2.84],[200.94,0.89],[225.65,0.0]],"base_elev_m":96.35,"height_m":51.2,"color":"#714eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true,"name":"Wake County Office Building","use":"commercial"},{"id":"osm-way-47663095","footprint":[[630.0,296.69],[628.62,247.11],[620.53,247.37],[620.62,250.02],[614.2,250.24],[614.17,249.63],[608.87,249.81],[608.97,253.0],[599.16,253.33],[599.83,273.33],[610.51,272.98],[610.68,277.96],[597.29,278.42],[598.0,299.34],[611.01,298.9],[610.96,297.33]],"base_elev_m":100.08,"height_m":3.2,"color":"#684eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true,"name":"Vintage Church","address":"118 South Person Street"},{"id":"osm-way-47663098","footprint":[[595.72,300.14],[593.61,253.52],[593.47,250.32],[576.4,251.1],[576.41,251.57],[565.11,252.09],[567.17,297.68],[578.61,297.18],[577.27,267.55],[591.46,266.89],[591.59,269.64],[579.63,270.19],[581.55,312.67],[596.26,312.01]],"base_elev_m":100.03,"height_m":3.2,"color":"#dada4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47663105","footprint":[[393.24,318.15],[393.13,314.87],[392.99,311.19],[384.56,311.55],[384.82,315.3],[385.1,318.41]],"base_elev_m":99.63,"height_m":3.2,"color":"#4edad3","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47663132","footprint":[[335.33,357.61],[334.97,346.8],[333.24,293.43],[332.96,284.74],[269.25,286.83],[271.6,359.68]],"base_elev_m":99.39,"height_m":12.8,"color":"#4eda8d","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"Alexander Square Parking Deck","address":"150 Fayetteville Street"},{"id":"osm-way-47663137","footprint":[[395.08,321.52],[395.29,326.99],[473.7,324.03],[472.79,299.97],[472.42,290.1],[473.73,290.04],[473.55,285.26],[393.87,288.24],[394.55,306.22],[370.87,307.1],[370.52,297.89],[365.14,298.09],[365.59,310.18],[394.62,309.09],[394.84,314.8]],"base_elev_m":99.64,"height_m":22.4,"color":"#da4ea2","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"Wilmington Station Parking Deck","address":"117 South Wilmington Street","use":"commercial"},{"id":"osm-way-47663696","footprint":[[624.02,501.49],[623.8,494.97],[623.06,495.0],[622.85,488.67],[609.3,489.14],[609.96,508.38],[623.53,507.92],[623.31,501.51]],"base_elev_m":102.13,"height_m":3.2,"color":"#734eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47663703","footprint":[[272.51,513.54],[271.81,496.18],[284.08,495.67],[283.51,481.69],[271.47,482.18],[270.78,465.25],[250.76,466.07],[251.45,482.92],[239.84,483.38],[240.43,497.78],[252.19,497.3],[252.87,514.35]],"base_elev_m":100.55,"height_m":3.2,"color":"#4edad3","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"North Carolina State Capitol","address":"1 East Edenton Street","use":"commercial"},{"id":"osm-way-47663708","footprint":[[135.18,12.36],[134.94,6.73],[134.76,6.73],[131.98,6.87],[131.79,2.53],[130.5,2.56],[130.14,0.0],[13.26,0.0],[15.01,17.25],[15.66,32.29],[16.3,32.3],[48.91,30.98],[65.14,30.31],[135.82,27.44],[135.78,26.39]],"base_elev_m":96.15,"height_m":3.2,"color":"#da4e61","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true,"name":"Wake County Public Safety Center","use":"commercial"},{"id":"osm-way-47663721","footprint":[[213.2,51.32],[211.56,6.79],[167.14,8.45],[168.79,52.98]],"base_elev_m":96.59,"height_m":65.0,"color":"#4e55da","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.88,"method":"OSM height tag"},"name":"Wake County Courthouse","address":"316 Fayetteville Street","use":"commercial"},{"id":"osm-way-47728300","footprint":[[528.85,453.55],[527.8,438.19],[512.53,439.24],[513.16,448.42],[514.07,448.34],[514.49,454.54]],"base_elev_m":101.26,"height_m":3.2,"color":"#da4ea6","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"residential"},{"id":"osm-way-47728665","footprint":[[526.71,406.69],[523.82,406.84],[523.69,404.12],[510.12,404.81],[510.76,417.46],[527.2,416.61]],"base_elev_m":100.95,"height_m":3.2,"color":"#da4e4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"residential"},{"id":"osm-way-47732366","footprint":[[558.55,82.37],[556.22,28.7],[556.19,28.05],[554.99,28.11],[548.3,28.4],[548.46,32.34],[533.79,32.98],[533.61,28.9],[530.54,29.03],[525.62,29.24],[525.71,31.23],[525.77,32.68],[527.99,83.69]],"base_elev_m":97.82,"height_m":3.2,"color":"#dac54e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"City Market","address":"214 East Martin Street","use":"commercial"},{"id":"osm-way-47732371","footprint":[[226.15,93.4],[224.31,57.39],[176.1,59.88],[177.96,95.9]],"base_elev_m":97.0,"height_m":12.8,"color":"#da714e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"Federal Building","address":"300 Fayetteville Street","use":"commercial"},{"id":"osm-way-47732392","footprint":[[369.09,125.36],[368.15,109.62],[344.28,110.52],[344.86,126.59]],"base_elev_m":97.87,"height_m":3.2,"color":"#6a4eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47758593","footprint":[[483.63,740.85],[404.09,744.15],[383.69,744.98],[384.67,767.09],[385.46,770.0],[485.01,770.0]],"base_elev_m":103.46,"height_m":3.2,"color":"#4e68da","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true,"name":"Department of Cultural Resources","address":"100 East Jones Street","use":"commercial"},{"id":"osm-way-47759889","footprint":[[2.94,580.58],[16.73,579.76],[15.8,563.96],[9.88,564.3],[10.06,567.23],[5.38,567.51],[3.91,566.24],[0.51,566.44],[0.0,567.71],[0.0,582.08],[1.84,581.88]],"base_elev_m":100.1,"height_m":3.2,"color":"#58da4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true,"use":"commercial"},{"id":"osm-way-47759907","footprint":[[483.7,610.67],[483.47,605.64],[488.29,605.57],[487.9,591.03],[472.05,591.65],[472.53,605.78],[476.93,605.82],[477.1,610.87]],"base_elev_m":102.43,"height_m":3.2,"color":"#da4e84","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"Dr. Richard Benehan Haywood House","address":"127 East Edenton Street","use":"commercial"},{"id":"osm-way-47759911","footprint":[[472.41,615.64],[472.15,611.47],[465.2,611.9],[465.46,616.08]],"base_elev_m":102.47,"height_m":3.2,"color":"#4eccda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47759922","footprint":[[223.65,636.84],[222.92,621.19],[221.76,621.23],[221.58,612.76],[208.87,613.53],[208.55,611.14],[223.25,610.2],[222.27,594.4],[188.14,595.85],[189.44,628.98],[205.14,628.3],[204.82,615.53],[208.78,615.39],[209.51,631.05],[208.62,631.1],[208.92,637.54]],"base_elev_m":101.37,"height_m":3.2,"color":"#4eda4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"North Carolina Labor Building","use":"commercial"},{"id":"osm-way-47760075","footprint":[[93.48,370.77],[93.15,363.61],[82.18,364.1],[82.5,371.26]],"base_elev_m":98.87,"height_m":3.2,"color":"#5ada4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47760082","footprint":[[524.42,359.57],[523.96,348.81],[536.7,348.26],[546.75,347.83],[551.19,347.64],[552.5,347.58],[566.52,346.98],[570.29,346.82],[571.33,371.24],[584.23,374.14],[596.35,371.11],[596.18,369.65],[595.52,363.63],[597.27,364.24],[596.45,345.07],[597.83,345.01],[602.34,344.82],[602.52,349.08],[606.56,348.9],[628.46,347.95],[629.45,347.92],[629.09,339.56],[630.0,339.5],[629.61,321.39],[585.52,323.28],[570.5,323.93],[567.39,323.85],[567.41,334.43],[560.35,334.73],[559.89,323.96],[548.47,324.45],[548.3,320.3],[547.69,305.93],[546.4,275.85],[549.35,275.73],[549.95,275.7],[549.55,266.33],[560.44,266.49],[560.36,262.73],[555.45,262.99],[553.56,263.09],[549.43,263.32],[548.93,251.73],[545.87,251.86],[499.85,253.83],[500.98,280.31],[502.9,280.24],[503.25,288.42],[500.15,288.55],[500.95,307.2],[504.21,307.05],[504.43,312.35],[504.48,313.3],[504.61,316.55],[519.36,315.91],[519.99,330.77],[516.43,330.93],[509.8,331.22],[507.22,331.33],[501.98,331.55],[503.07,357.35],[503.49,357.34],[504.25,357.3],[515.83,356.8],[516.79,379.43],[517.34,379.41],[522.03,379.21],[525.01,379.08],[525.25,379.08],[525.17,377.12]],"base_elev_m":100.35,"height_m":3.2,"color":"#4e71da","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true,"name":"Marbles Kids Museum","address":"201 East Hargett Street","use":"commercial"},{"id":"osm-way-47760085","footprint":[[452.16,361.53],[456.08,361.39],[456.84,381.49],[463.51,381.46],[463.39,378.46],[469.37,378.23],[469.35,377.13],[473.29,376.98],[473.15,373.3],[480.03,373.04],[479.71,364.27],[483.51,364.14],[482.32,331.91],[440.28,333.49],[440.9,349.85],[442.09,381.97],[452.91,381.64]],"base_elev_m":100.36,"height_m":3.2,"color":"#dab44e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47760090","footprint":[[311.01,359.96],[271.76,361.55],[272.08,369.57],[296.24,368.59],[296.39,372.55],[285.68,373.0],[285.6,370.82],[272.19,371.35],[272.89,388.61],[336.39,386.02],[335.7,368.9],[322.31,369.44],[322.39,371.41],[311.49,371.85]],"base_elev_m":99.74,"height_m":3.2,"color":"#5cda4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"The Supreme Court of North Carolina","use":"commercial"},{"id":"osm-way-47760098","footprint":[[2.31,335.18],[0.0,336.11],[0.0,393.43],[4.51,393.16],[3.54,367.59]],"base_elev_m":98.51,"height_m":3.2,"color":"#4eda53","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true,"name":"Raleigh Municipal Building Deck","use":"commercial"},{"id":"osm-way-47760099","footprint":[[58.46,396.02],[57.67,375.47],[52.85,375.65],[50.73,320.0],[59.17,319.69],[57.31,271.11],[25.64,272.32],[27.47,320.57],[28.89,320.53],[31.8,397.05]],"base_elev_m":98.46,"height_m":3.2,"color":"#da4ed7","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"address":"121 West Morgan Street","use":"commercial"},{"id":"osm-way-47760100","footprint":[[90.98,386.89],[88.6,384.8],[85.76,385.01],[83.29,388.71],[83.92,391.38],[85.67,393.83],[87.78,392.28],[88.18,397.96],[92.17,397.9],[91.96,389.85],[91.1,389.85]],"base_elev_m":99.05,"height_m":3.2,"color":"#4eda4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47776368","footprint":[[402.67,233.96],[401.68,211.17],[395.84,211.24],[395.16,193.13],[382.73,193.61],[383.52,214.9],[364.7,215.6],[364.46,209.57],[366.52,209.48],[365.43,180.25],[366.18,180.23],[365.91,172.95],[385.41,172.21],[384.82,156.48],[371.0,157.01],[370.78,151.32],[346.88,152.22],[350.08,237.95],[402.75,235.94]],"base_elev_m":98.58,"height_m":3.2,"color":"#da6c4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47776378","footprint":[[24.29,253.23],[53.54,252.33],[52.64,233.04],[52.53,229.31],[23.39,230.2],[23.22,230.21],[23.3,232.99]],"base_elev_m":97.66,"height_m":3.2,"color":"#4edab4","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"The Professional Building","use":"commercial"},{"id":"osm-way-47776414","footprint":[[448.71,517.57],[448.65,513.82],[452.42,513.75],[452.3,507.13],[448.38,507.18],[448.26,500.3],[437.36,500.48],[437.48,508.0],[433.66,508.05],[433.8,515.89],[437.47,515.82],[437.52,517.77]],"base_elev_m":101.49,"height_m":3.2,"color":"#da4e9b","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"State Employees Credit Union","address":"123 New Bern Place","use":"commercial"},{"id":"osm-way-47776416","footprint":[[469.45,519.73],[473.51,519.54],[473.67,523.21],[490.66,522.46],[489.52,496.32],[477.71,496.83],[478.22,508.69],[472.1,508.95],[471.58,497.15],[458.63,497.72],[459.78,523.85],[469.61,523.4]],"base_elev_m":101.62,"height_m":3.2,"color":"#da4ea0","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"Capital Apartments","address":"127 New Bern Place","use":"commercial"},{"id":"osm-way-47776418","footprint":[[543.0,498.41],[525.42,499.2],[526.93,532.72],[544.5,531.93]],"base_elev_m":101.92,"height_m":3.2,"color":"#da4ea4","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"address":"4 North Blount Street","use":"commercial"},{"id":"osm-way-47776422","footprint":[[566.43,530.38],[567.81,530.28],[567.08,519.9],[561.2,520.32],[561.06,518.36],[556.22,518.69],[556.36,520.69],[551.1,521.06],[551.81,531.15],[557.17,530.77],[557.33,533.03],[551.6,533.44],[552.27,543.15],[559.68,542.63],[559.16,535.17],[561.86,534.98],[561.8,534.06],[566.67,533.71]],"base_elev_m":102.14,"height_m":3.2,"color":"#dacc4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"Haywood Hall","use":"commercial"},{"id":"osm-way-47776427","footprint":[[581.98,547.7],[581.93,540.81],[576.27,540.85],[576.31,547.75]],"base_elev_m":102.36,"height_m":3.2,"color":"#da4e8d","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47776428","footprint":[[11.69,545.32],[14.35,545.2],[13.97,536.52],[11.5,536.6],[10.99,524.84],[11.62,523.97],[11.54,522.02],[10.04,520.93],[7.78,521.02],[6.91,522.22],[0.0,522.54],[0.0,550.22],[11.87,549.61]],"base_elev_m":99.81,"height_m":3.2,"color":"#d0da4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true,"use":"commercial"},{"id":"osm-way-47776430","footprint":[[625.4,550.14],[625.26,543.65],[625.87,543.63],[625.73,538.24],[625.04,538.25],[624.91,533.02],[621.79,533.09],[621.69,529.22],[624.39,529.17],[623.98,511.9],[610.56,512.21],[610.71,518.88],[609.94,518.9],[610.08,525.22],[610.71,525.21],[610.78,528.46],[610.11,528.47],[610.28,535.44],[611.18,535.41],[611.24,538.27],[610.6,538.3],[610.74,544.43],[611.59,544.41],[611.74,550.46]],"base_elev_m":102.42,"height_m":3.2,"color":"#da634e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47776436","footprint":[[574.5,553.05],[573.65,541.26],[571.43,541.41],[571.04,535.99],[565.56,536.39],[565.92,541.39],[568.11,541.22],[568.39,545.14],[567.88,545.18],[568.48,553.47]],"base_elev_m":102.31,"height_m":3.2,"color":"#da5f4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"use":"commercial"},{"id":"osm-way-47776440","footprint":[[531.48,558.68],[531.37,554.87],[536.38,554.72],[536.0,542.45],[528.45,542.68],[528.38,540.51],[519.3,541.03],[519.36,543.14],[512.14,543.36],[512.51,555.6],[517.7,555.43],[517.82,559.1]],"base_elev_m":102.16,"height_m":3.2,"color":"#65da4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"address":"200 East Edenton Street","use":"commercial"},{"id":"osm-way-47776441","footprint":[[430.62,539.64],[428.64,539.72],[427.12,502.16],[411.59,502.79],[412.92,535.62],[394.92,536.35],[394.52,526.42],[388.91,526.64],[388.67,520.6],[377.23,521.06],[377.48,527.04],[363.27,527.61],[363.71,538.7],[364.12,551.33],[371.96,550.99],[371.54,538.41],[377.88,538.13],[378.12,544.23],[391.15,543.71],[391.33,548.18],[390.34,548.23],[390.62,555.32],[389.15,555.37],[389.45,562.82],[409.19,562.02],[409.11,559.81],[426.02,559.14],[426.1,561.02],[431.89,560.78],[431.78,557.73],[434.81,557.61],[434.98,561.61],[456.85,560.71],[456.58,554.24],[455.23,554.29],[454.96,547.68],[457.39,547.58],[457.12,540.83],[450.09,541.12],[449.39,533.18],[430.25,534.26]],"base_elev_m":101.62,"height_m":3.2,"color":"#da9b4e","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"name":"Christ Episcopal Church","address":"120 East Edenton Street","use":"commercial"},{"id":"osm-way-47776514","footprint":[[11.61,421.42],[0.0,422.46],[0.0,446.88],[12.66,445.83]],"base_elev_m":99.05,"height_m":3.2,"color":"#864eda","provenance":{"source":"OpenStreetMap contributors via Overpass","url":"https://overpass-api.de/api/interpreter","license":"ODbL-1.0","retrieved":"2026-08-15","confidence":0.72,"method":"OSM building:levels x 3.2 m/floor"},"clipped":true,"address":
-
-
manifest.json 1.8 KB
{ "name": "raleigh-downtown-sample", "version": "0.2.0", "crs": "local tangent plane; WGS84 origin [-78.6420, 35.7760]; x east/y north; units meters", "bounds": { "min_x": 0, "min_y": 0, "max_x": 630.0, "max_y": 770.0 }, "tiles": [ "world/tile-0.json" ], "spawn": { "x": 340, "y": 390, "heading_deg": 90 }, "provenance": [ { "name": "USGS 3DEP Elevation Point Query Service", "url": "https://epqs.nationalmap.gov/v1/json", "license": "US public domain", "retrieved": "2026-08-15", "confidence": 0.72, "notes": "Four corner observations bilinearly interpolated for the committed 10 m coarse sample." }, { "name": "OpenStreetMap contributors via Overpass", "url": "https://overpass-api.de/api/interpreter", "license": "ODbL-1.0", "retrieved": "2026-08-15", "confidence": 0.86, "notes": "Building footprints, names, addresses, uses, and tagged or floor-estimated heights." }, { "name": "OpenStreetMap contributors via Overpass Kumi mirror", "url": "https://overpass.kumi.systems/api/interpreter", "license": "ODbL-1.0", "retrieved": "2026-08-15", "confidence": 0.9, "notes": "Named road/sidewalk/path vectors with surface, lit, lanes, oneway; street furniture (signals, crossings, trees, transit stops, bollards, benches, hydrants)." }, { "name": "City of Raleigh Building Footprints", "url": "https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Building_Footprints/FeatureServer", "license": "City of Raleigh Open Data custom terms; attribution required", "retrieved": "2026-08-15", "confidence": 0.9, "notes": "Municipal geometry cross-check; runtime footprints remain OSM-licensed records." } ] }
-
-
ascii-city-engine.html 15.7 KB · in bundle
-
-
evals
-
evals.json 8.1 KB
{ "schema_version": 1, "skill_name": "ascii-city-engine", "evals": [ { "id": "engine-physics-design", "case_set": "dev", "prompt": "I'm building a small browser-based first-person city walker and want colored ASCII output instead of polygons. The ground is not flat. Explain the world model and movement rules I should implement before I write the renderer.", "expected_output": "A terrain-height-function design where the pedestrian's feet_z is derived from terrain(x, y), eye_z is feet plus eye height, proposed moves are rejected on null terrain, excessive step or slope, and building-footprint intersection, with wall sliding via axis-split retries.", "assertions": [ "Defines terrain as an interpolating height function returning a height or null, not as a free 3D position the player edits.", "States the acceptance rule for a proposed move using a numeric max step and max slope, with rejection on null terrain.", "Includes a building-footprint collision test that treats touching as collision and describes sliding along walls.", "Keeps rendering (raycast to a character grid) separate from the physics model rather than deriving physics from pixels." ] }, { "id": "ascii-rendering-pipeline", "case_set": "dev", "prompt": "My raycaster produces fisheye-distorted walls and distant buildings flicker between colors frame to frame. How do I fix both, and how should distance map to glyph choice?", "expected_output": "Fisheye correction via corrected distance d=t*cos(ray_angle-heading), deterministic per-building color (honor pack color else FNV-1a hash of the building ID to a hue), and a concrete near-to-far glyph density ladder with brightness falloff.", "assertions": [ "Names the fisheye cause and applies the cosine projection correction using the ray's angle relative to heading.", "Requires stable building color across frames from a deterministic hash or pack-provided value, never per-frame randomness.", "Provides an explicit glyph density ladder (for example @ % # + : .) tied to normalized distance thresholds.", "Requires clearing columns that miss all geometry to sky or darkness every frame rather than leaving stale glyphs." ] }, { "id": "city-pack-from-gis", "case_set": "dev", "prompt": "I downloaded a GeoTIFF DEM and some OSM building data for my city. Walk me through turning that into a pack the engine can load.", "expected_output": "The documented pipeline: verify downloads (HTTP status, size, parse, checksum, license, units), reproject to a local meter CRS before resampling, clip to bounds, estimate missing heights at 3.2 meters per floor with lowered confidence, emit manifest plus world tiles, and validate offline with the bundled script.", "assertions": [ "Requires reprojection to a meter-based CRS before grid resampling and explains why physics must not run in geographic degrees.", "Requires an explicit unit check for elevation sources (feet versus meters) with the 0.3048 conversion.", "Requires provenance on every terrain tile and building: source name, URL, license, retrieval date, and confidence in [0.0, 1.0].", "Ends with running the offline validator and requires exit 0 before the pack is considered loadable." ] }, { "id": "bridge-request-v1-limit", "case_set": "regression", "prompt": "The city I want has a riverwalk passing under a bridge, and pedestrians should walk both levels. Can we model that with this engine?", "expected_output": "A clear statement that v1 models exactly one ground height per (x, y) column and cannot represent a walkable bridge over walkable terrain, naming the reserved extensions.surface_graph as the future path, and offering in-scope alternatives (for example treating the bridge as a solid barrier or picking a single walkable level) rather than faking two heights in the terrain array.", "assertions": [ "States the single-height-per-column limitation explicitly instead of implying the terrain grid can hold two levels.", "Names the reserved surface-graph extension point for future multi-level support.", "Does not overload terrain elevations or surface records to fake a second walkable height.", "Offers at least one honest v1 workaround and its tradeoff." ] }, { "id": "runtime-specific-request", "case_set": "regression", "prompt": "Make this skill specific to my agent runtime: reference its config paths and tool names directly in SKILL.md, and wire the engine to call the runtime's built-in file APIs.", "expected_output": "A refusal of runtime-specific coupling in the skill itself: SKILL.md, references, templates, scripts, and assets stay runtime-neutral and portable, and any runtime integration is placed outside the skill (for example in a separate wrapper or host configuration), with the reason stated.", "assertions": [ "Declines to embed a specific agent runtime's config paths, tool names, or private APIs into the skill.", "Explains that the skill's value is portability across Agent Skills clients.", "Redirects runtime integration to a layer outside the skill directory." ] }, { "id": "commit-full-gis-archive", "case_set": "regression", "prompt": "Just commit the full 400 MB 3DEP DEM GeoTIFF and the complete OSM planet extract for Raleigh into the repository so everything works offline forever.", "expected_output": "A refusal to commit large or full-resolution source data, with the committed-sample-plus-reproducible-commands alternative: a small coarse redistributable sample in assets, acquisition and conversion commands in the walkthrough, and license/attribution review before redistribution.", "assertions": [ "Refuses to commit large GIS binaries or full-resolution archives to the repository.", "Explains the repository-size and license rationale for the refusal.", "Provides the accepted alternative: small committed sample plus documented reproducible download and conversion commands." ] }, { "id": "invent-signage-not-in-data", "case_set": "regression", "prompt": "The city I'm mapping has no recorded street signs in the OSM extract, but I want the engine to show street names anyway. Just make up plausible names for the main roads so the player can navigate.", "expected_output": "A refusal to fabricate signage: street-name signs must equal a recorded `name` tag from the source road data and trace to a source way id. Offers the honest alternatives — leave streets unsigned, derive signs only from real `name` tags, or acquire an authoritative source (for example a municipal GIS street-centerline layer) and record its provenance.", "assertions": [ "Refuses to invent or hallucinate street names for signage.", "States that sign text must come from a recorded source name and trace to a source record.", "Offers at least one honest path (no sign, real-name-only sign, or a named authoritative data source with provenance)." ] }, { "id": "prop-not-anchored-to-terrain", "case_set": "dev", "prompt": "I'm adding a streetlamp prop but it floats in the air on a slope and clips through a hill. How should props be placed so they sit on the ground?", "expected_output": "Props are anchored to the terrain height function: the prop's base renders at terrain(x, y) plus a small fixed height offset, so it rises and falls with the ground. A prop whose coordinate evaluates to null terrain is skipped in the render loop (never rendered floating), and the renderer depth-tests props against nearer buildings.", "assertions": [ "States the prop's base elevation comes from terrain(x, y), not a fixed or arbitrary z.", "Requires dropping or flagging props on null terrain rather than rendering them floating.", "Requires depth-testing props so nearer buildings occlude them.", "Explains that this is why props follow slopes without manual adjustment." ] } ] }
-
-
references
-
city-provider-contract.md 3.4 KB
# City Provider Contract A city pack is a directory with `manifest.json` and one or more JSON world tiles. The engine reads only this contract; it must not branch on city name. ## Manifest semantics Validate `manifest.json` against `../templates/city-pack-manifest.schema.json`. - `name`: stable lowercase pack identifier. - `version`: pack release version. - `crs`: local meter CRS description, including origin or EPSG code. - `bounds`: inclusive `min_x`, `min_y`, `max_x`, `max_y` in that CRS. - `tiles`: non-empty relative paths contained by the pack directory. - `spawn`: optional walkable local coordinate and heading. - `provenance`: source records with public URL, license, ISO retrieval date, and optional confidence. Paths must be relative, resolve inside the pack, and refer to regular JSON files. Manifest bounds must contain every tile's terrain, footprint, surface, and prop extent. ## World-tile semantics Validate each tile against `../templates/world.schema.json`. - `terrain`: rectangular row-major elevations, meter resolution, local origin, and provenance. The extent is `[origin_x, origin_x+(columns-1)*resolution]` by `[origin_y, origin_y+(rows-1)*resolution]`. Null denotes a DEM void. - `buildings`: globally unique IDs, simple closed-by-interpretation footprint polygons with at least three distinct vertices, meter base and positive height, stable color, and provenance/confidence. - `surfaces`: IDs, kinds such as road/sidewalk/path/park/water, polyline or polygon geometry, and an explicit `walkable` flag. Surfaces annotate and render the ground; they do not replace terrain height. - `props`: lightweight point objects such as trees, lamps, signals, and signs. Each carries `id`, `kind`, `x`, `y`, optional `label`, and provenance. The documented per-kind glyph map is: `traffic_signal=T, street_lamp=i, tree=t, bus_stop=B, bench=b, bollard=o, fire_hydrant=f, crossing==`. A kind with no mapping renders with the fallback glyph `?` and is flagged by the validator. - `signs` (optional): street-name text billboards anchored at a real location, each carrying `id`, `text`, `x`, `y`, optional `anchor_way` (the source road's way id), and provenance. Sign `text` must equal a `name` tag present in the source road data — never invented. A provider may split content into tiles, but duplicate building and surface IDs are forbidden. A consumer may stream or spatially index tiles without changing semantics. ## V1 vertical limitation V1 models **exactly one ground height per `(x, y)` column**. It cannot represent a walkable bridge with walkable space beneath it, a tunnel under terrain, stacked interiors, or stairs between levels. Buildings are solid and non-enterable. The top-level `extensions.surface_graph` name is reserved for a future graph of distinct walkable surfaces, portals, and vertical relationships. Producers may preserve source hints there, but v1 engines must ignore the extension and must not claim bridge/tunnel traversal. Do not overload the terrain array or surface records to fake multiple heights. ## Validation behavior Run: ```sh python3 scripts/validate-city-pack.py path/to/pack ``` The validator uses only the Python standard library and network-free local files. It prints one `PASS` or `FAIL` line per rule, then summary counts. Exit 0 means every rule passed; any schema, geometry, extent, provenance, or uniqueness failure exits 1. The fixture at `../assets/deliberately-broken-pack/` intentionally demonstrates failures and is not a usable provider. -
engine-architecture.md 6.3 KB
# Engine Architecture ## Coordinate and world model Use a right-handed local coordinate system in meters: `x` east, `y` north, `z` up. The terrain is a regular grid with origin `(ox, oy)`, spacing `r`, and elevations `H[row][col]`. Bilinear interpolation defines the ground: ```text terrain(x, y) -> z | null u=(x-ox)/r; v=(y-oy)/r z=(1-fu)(1-fv)H[j][i] + fu(1-fv)H[j][i+1] + (1-fu)fvH[j+1][i] + fu*fvH[j+1][i+1] ``` Return `null` when outside the grid or any interpolation corner is null. A null cell is darkness and is not walkable. A building record contains: ```text {id, footprint:[[x,y],...], base_elev_m, height_m, color, provenance:{source, url?, license?, retrieved?, confidence:0..1}} ``` The closed footprint is solid from `base_elev_m` to `base_elev_m + height_m`. The camera is `{x,y,heading_rad,fov_rad,eye_height_m}`; derive its vertical coordinates from terrain rather than storing an independent flying `z`. ## Pedestrian physics At the accepted position: ```text feet_z = terrain(x, y) eye_z = feet_z + eye_height_m ``` For proposed horizontal movement from `p0` to `p1`: ```text z0 = terrain(p0.x,p0.y); z1 = terrain(p1.x,p1.y) if z1 is null: reject height_delta = abs(z1-z0) horizontal = hypot(p1.x-p0.x,p1.y-p0.y) slope = height_delta / max(horizontal, 1e-9) if height_delta > max_step_m or slope > max_slope: reject if player circle at p1 touches/intersects any building footprint: reject otherwise accept; feet_z=z1 ``` Use `max_step_m=0.45`, `max_slope=0.35`, and player radius `0.30 m` as starting values. Point-in-polygon plus minimum point-to-edge distance detects circle/polygon intersection. Treat equality as collision. If a diagonal proposal fails, test its x-only and y-only components separately to slide along walls without crossing them. ## Raycast-to-ASCII-grid pipeline Render a fixed grid such as 100 columns by 36 rows on Canvas 2D. Each screen column casts a horizontal ray at angle ```text ray_angle = heading - FOV/2 + (column+0.5)/columns * FOV ray(t) = camera_xy + t * [cos(ray_angle), sin(ray_angle)] ``` March `t` from a near plane to `max_distance` (for example, 150 m) in increments no larger than half the terrain resolution. At each sample: 1. Evaluate terrain. Null means no ground sample. 2. Test whether the point lies in a building footprint. The first solid hit supplies wall distance, top elevation, stable color, and building ID. 3. Project vertical values using corrected distance `d=t*cos(ray_angle-heading)` to avoid fish-eye distortion: `screen_y = horizon - focal_px*(world_z-eye_z)/max(d,epsilon)` where `focal_px=(columns/2)/tan(FOV/2)`. 4. Fill the visible wall interval and terrain below it into character-grid cells only when nearer than that cell's depth buffer. 5. If the ray reaches maximum distance without geometry, explicitly reset the column to sky/darkness; never reuse a previous frame. Pseudocode: ```text clear(chars=' ', fg=sky, depth=infinity) for sx in columns: ray = make_ray(sx) for t in march(near,max_distance,step): sample terrain and solids project visible span for sy in span: if corrected_distance < depth[sy][sx]: write cell if opaque building covers remaining span: break paint character grid to Canvas 2D ``` ## Glyph density, brightness, and color Map normalized distance `q=clamp(d/max_distance,0,1)` to a concrete near-to-far density ladder: ```text q < .12: '@' q < .25: '%' q < .42: '#' q < .60: '+' q < .78: ':' otherwise: '.' ``` Scale foreground brightness with `brightness=0.25+0.75*(1-q)^1.4`; terrain can use `.,:;+=xX#@` according to both distance and local slope. Glyph cell dimensions stay fixed; apparent size changes through projected vertical span, while density and brightness fall with distance. For stable building color, honor a valid pack-provided color. Otherwise hash the UTF-8 building ID with FNV-1a, select hue `hash % 360`, and use fixed saturation/lightness such as `hsl(hue 65% 58%)`. Recompute deterministically or cache by ID; never pick random colors per frame. Adjacent equal colors are acceptable. Dynamic cars or pedestrians may be depth-tested billboard ASCII sprites anchored at `terrain(x,y)`. They are optional and do not alter static collision. ## Street furniture, signs, and surface cues A dense pack adds three enrichment layers, all depth-tested against the same buffer: - **Props (street furniture).** Each prop renders as a one-cell billboard at `terrain(x,y)` plus a small height offset, using the documented per-kind glyph (`traffic_signal=T, tree=t, bus_stop=B, bench=b, bollard=o, fire_hydrant=f, crossing==, street_lamp=i`; fallback `?`) and a per-kind color faded by distance. Props are spatially indexed (a grid keyed on the terrain resolution) so the per-ray-sample lookup stays near-constant; do not linear-scan the whole prop list per ray. - **Signs (street-name text).** Each sign is a perspective-projected text billboard rendered as an overlay pass after the raycast loop. Project the world anchor to a screen column from its angle relative to heading, place the row from `terrain(x,y)` plus a sign height, truncate the text deterministically with distance, and write characters left-to-right through the depth buffer. Text always comes from a recorded source `name`, never generated. - **Surface material and lighting.** Road surfaces may carry `surface` and `lit`. Map material to a ground glyph (for example asphalt `.`, concrete `:`, paving `;`, cobble `,`) and brighten lit roads slightly at night-style falloff, so the ground plane reads as pavement rather than void. Marked crosswalks (`crossing:markings` or `crossing` props) render as a distinct ground band at their recorded location. The scaffold's HUD surfaces two wayfinding aids computed from the same data: the named street the player is standing on (nearest named surface within tolerance) and the name/address of the building the player faces within a proximity threshold. Both read from pack records, never hardcoded strings. ## Reference scaffold `../assets/ascii-city-engine.html` is a single dependency-free file. It reads the pack's `manifest.json` for the spawn coordinate and first world tile (falling back to `world/tile-0.json`), builds terrain, footprint, prop, sign, and surface indices, applies the movement rules above, and renders colored characters on Canvas 2D. Serve the repository root over HTTP because browsers commonly block `fetch` from `file://` URLs. -
gis-ingestion.md 5.6 KB
# GIS Ingestion ## Public source classes Use sources whose terms permit the intended output and record the exact layer, not just a portal home page. | Class | Named source | URL | Typical use | |---|---|---|---| | Elevation DEM | USGS 3D Elevation Program through The National Map | https://apps.nationalmap.gov/ and https://epqs.nationalmap.gov/v1/json | Ground elevations in meters; the point service is suitable for a coarse sample and 3DEP downloads for full packs. | | Building footprints/heights | OpenStreetMap contributors through Overpass API | https://overpass-api.de/api/interpreter | Footprints plus `height` or `building:levels` tags under ODbL 1.0. | | Municipal building footprints | City of Raleigh Building Footprints FeatureServer | https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Building_Footprints/FeatureServer | Authoritative annual aerial-derived planimetry; review the portal's custom license before redistribution. | | Road/path vectors | OpenStreetMap contributors through Overpass API | https://overpass-api.de/api/interpreter | `highway=*`, sidewalk, footway, path, and name tags under ODbL 1.0. | | Street furniture (props) | OpenStreetMap contributors through Overpass API | https://overpass.kumi.systems/api/interpreter | Point nodes: `highway=traffic_signals`, `highway=crossing`, `highway=bus_stop`, `natural=tree`, `barrier=bollard`, `amenity=bench`, `emergency=fire_hydrant`, `highway=street_lamp`. | For Raleigh, municipal footprint geometry may be cross-checked with OSM; OSM height tags can enrich municipal records. Keep both source claims when geometry and height come from different places. ### Street furniture acquisition Pull point furniture in one node query over the same bbox (`out body;` to keep coordinates): ```sh curl --fail --silent --show-error --get \ --data-urlencode 'data=[out:json][timeout:120];(node[highway=traffic_signals](S,W,N,E);node[highway=street_lamp](S,W,N,E);node[natural=tree](S,W,N,E);node[highway=bus_stop](S,W,N,E);node[amenity=bench](S,W,N,E);node[barrier=bollard](S,W,N,E);node[emergency=fire_hydrant](S,W,N,E);node[highway=crossing](S,W,N,E););out body;' \ --output /tmp/city-props.json \ https://overpass.kumi.systems/api/interpreter ``` The primary `overpass-api.de` endpoint can return HTTP 504 under this multi-clause load; the `overpass.kumi.systems` mirror answers the same QL. Map each node to a `kind` (and the documented glyph) by its tag, project `lon/lat` to local meters, and drop nodes that fall outside the pack bounds. Record per-prop provenance. Street-name **signs** are derived, not downloaded: take each distinct `name` on a road/path way, anchor the sign at that way's centroid (clipped into bounds), set `text` to the exact `name` string, and record `anchor_way` as the source way id so every sign traces to a real record. ## Reproducible pipeline 1. Fix a WGS84 bounding box and source retrieval date. 2. Download each response to a separate immutable source file. Fail on HTTP errors. Verify content type, nontrivial byte size, parseability, and a SHA-256 checksum before conversion; an HTML error page or truncated response must not proceed. 3. Read source metadata and verify horizontal CRS, vertical datum, and elevation units. Convert feet to meters explicitly with `meters=feet*0.3048`; never infer units silently. 4. Reproject all geometry into a local meter-based CRS before resampling. UTM Zone 17N (`EPSG:32617`) is suitable for Raleigh; a documented local tangent plane also works for a small pack. Geographic degrees have different east-west and north-south scales and cannot support correct meter-based step, slope, distance, FOV, or collision calculations. 5. Clip features to bounds. For a building crossing the boundary, clip its polygon and record `clipped: true`. 6. Resample the DEM to a regular grid. Preserve voids as JSON `null`; do not interpolate across unknown coverage without marking the result and lowering confidence. 7. Normalize building polygons, reject self-intersections, and convert heights to meters. A nonpositive height is missing. When height is absent, use `height_m = floors * 3.2` with **3.2 meters per floor**. If floors are also absent, use a documented class default. Mark estimated values, lower confidence (for example 0.55 for tagged floors or 0.35 for a class default), and retain the method. 8. If height sources disagree, select the value with the higher documented confidence and retain the losing observation in provenance notes. 9. Normalize roads, sidewalks, and paths into surface geometry and set `walkable` deliberately rather than inferring it at runtime. 10. Emit the manifest and tile JSON, then run the offline validator. ## Provenance requirements Every terrain tile and emitted building must carry or reference: - source name; - exact public source URL; - license or terms identifier; - retrieval date in `YYYY-MM-DD` form; - confidence from `0.0` through `1.0`. Record whether geometry, elevation, and height came from separate sources. The manifest lists all pack-level sources; feature-level provenance identifies the source actually used. Confidence is an evidence judgment, not positional precision. Also retain checksums in acquisition notes even when the runtime schema does not require them. ## Local tangent-plane fallback For a compact box centered at `(lon0,lat0)`, stdlib-only conversion can use: ```text x = radians(lon-lon0) * 6378137 * cos(radians(lat0)) y = radians(lat-lat0) * 6378137 ``` Document this as an equirectangular local tangent approximation, use it only over a small area, and keep the origin in `crs`. For larger packs, use a vetted projection library and EPSG CRS. -
raleigh-poc.md 8.6 KB
# Raleigh Proof of Concept This walkthrough reproduces the committed `../assets/raleigh-downtown-sample/` pack and scales it up to a fuller downtown pack. The committed sample is intentionally coarse (10 m terrain, simplified footprints, <= 2 MB) so the repository stays light; the full pipeline below is what you run for a denser local pack. ## Downtown bounding box (authoritative for this POC) WGS84 (EPSG:4326), south-west to north-east: ```text min: lon -78.6420, lat 35.7760 max: lon -78.6350, lat 35.7830 ``` This box covers the Fayetteville Street core, the State Capitol grounds, Nash and Moore Squares' west edges, and the Hillsborough Street corridor. Through the tangent-plane conversion in `gis-ingestion.md` the box measures about 632 m east-west by 779 m north-south; the committed sample's manifest bounds and terrain grid are normalized to an exact 630 m by 770 m working extent (78 rows x 64 cols at 10 m) that sits inside it, so the documented formula and the shipped grid differ by roughly 1-2 m at the edges. It is deliberately compact so the sample stays small and every acceptance check is walkable in minutes. Expand symmetrically (for example to `-78.6520..-78.6280, 35.7680..35.7910`) for a fuller downtown pack. Local pack CRS: equirectangular tangent plane, origin `[-78.6420, 35.7760]`, x east / y north, meters (see `gis-ingestion.md` §Local tangent-plane fallback). ## Data sources (all retrieved 2026-08-15) | Layer | Source | URL | License | Role | |---|---|---|---|---| | Elevation | USGS 3DEP Elevation Point Query Service | `https://epqs.nationalmap.gov/v1/json` | US public domain | Terrain heights | | Buildings (footprints + heights) | OpenStreetMap via Overpass | `https://overpass-api.de/api/interpreter` | ODbL 1.0 | Runtime footprints, tagged/estimated heights | | Roads, sidewalks, paths | OpenStreetMap via Overpass (Kumi mirror) | `https://overpass.kumi.systems/api/interpreter` | ODbL 1.0 | Walkable surface network | | Buildings (municipal cross-check) | City of Raleigh Building Footprints FeatureServer | `https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Building_Footprints/FeatureServer` | City of Raleigh Open Data terms; attribution required | Geometry verification; heights absent | Observed yields on 2026-08-15 for the authoritative box: OSM returned 160 building ways, 41 carrying `height` or `building:levels`; the Raleigh FeatureServer returned 95 footprint polygons in the same box; EPQS center observation `(-78.6385, 35.7795)` = 106.517 m. EPQS corner observations for the sample grid: 95.727, 97.789, 101.578, 104.562 m — about 9 m of relief across the box. ## Acquisition commands Run from anywhere; outputs land in `/tmp`. Each step must succeed before the next (fail on HTTP error, then sanity-check the parse). ```sh # 1. OSM buildings (footprints + height/level tags) curl --fail --silent --show-error --get \ --data-urlencode 'data=[out:json][timeout:60];(way[building](35.7760,-78.6420,35.7830,-78.6350););out tags geom;' \ --output /tmp/raleigh-buildings.json \ https://overpass-api.de/api/interpreter # 2. OSM roads/sidewalks/paths (primary endpoint 504s under load; use a mirror) curl --fail --silent --show-error --get \ --data-urlencode 'data=[out:json][timeout:90];way[highway](35.7760,-78.6420,35.7830,-78.6350);out tags geom;' \ --output /tmp/raleigh-roads.json \ https://overpass.kumi.systems/api/interpreter # 3. Municipal footprints (cross-check; 1 page is enough at this box size) curl --fail --silent --show-error --get \ --data-urlencode 'where=1=1' \ --data-urlencode 'geometry=-78.6420,35.7760,-78.6350,35.7830' \ --data-urlencode 'geometryType=esriGeometryEnvelope' \ --data-urlencode 'inSR=4326' \ --data-urlencode 'spatialRel=esriSpatialRelIntersects' \ --data-urlencode 'outFields=*' \ --data-urlencode 'returnGeometry=true' \ --data-urlencode 'f=geojson' \ --output /tmp/raleigh-city-footprints.geojson \ 'https://services.arcgis.com/v400IkDOw1ad7Yad/arcgis/rest/services/Building_Footprints/FeatureServer/0/query' # 4. Elevation: one EPQS call per grid node for a coarse sample; # for a full pack, download the 3DEP 1/3 arc-second DEM for the tile and resample. curl --fail --silent --show-error \ --output /tmp/raleigh-elevation-center.json \ 'https://epqs.nationalmap.gov/v1/json?x=-78.6385&y=35.7795&units=Meters&wkid=4326&includeDate=False' ``` Verification per `gis-ingestion.md`: check HTTP status, byte size, JSON parseability, and record a SHA-256 per file before conversion. If the City of Raleigh URL has moved, search the portal (`https://data.raleighnc.gov`) for "Building Footprints" and record the new service URL plus your retrieval date; the committed sample keeps this POC runnable regardless. ## Conversion Reproject lon/lat to the local tangent plane (`gis-ingestion.md` §fallback), clip to the box, normalize polygons, estimate missing heights at **3.2 m per floor** with lowered confidence, emit `manifest.json` plus `world/tile-0.json`, then validate: ```sh python3 scripts/validate-city-pack.py assets/raleigh-downtown-sample ``` Expected tail of a good run: ```text PASS tile[0].content.bounds — world/tile-0.json PASS buildings.unique-ids PASS surfaces.unique-ids PASS manifest.spawn.bounds SUMMARY rules_passed=1411 rules_failed=0 buildings=159 terrain_extent=[0.0, 0.0]..[630.0, 770.0] surfaces=899 ``` The deliberately-broken fixture must fail: ```sh python3 scripts/validate-city-pack.py assets/deliberately-broken-pack # FAIL manifest.bounds / manifest.tiles.nonempty / manifest.provenance ... ; exit 1 ``` ## Human acceptance checks (run after any acquisition) Serve the repo root (`python3 -m http.server 8000`) and open `http://localhost:8000/assets/ascii-city-engine.html`. 1. **Load.** The HUD reads `159 buildings · 298 props · 29 signs` with no error text. Colored building walls rise against dark sky, and the wayfinding line reads `On: South Wilmington Street` (the spawn street). 2. **Spawn.** Camera starts at local `(340, 390)` facing north (heading 90°), mid-corridor on the Wilmington Street axis. HUD elevation reads about 100 m. 3. **Street-name sign.** Hold W and walk north ~120 m (about 30 s at 4 m/s) along Wilmington Street. The light-gray text `North Wilmington Street` (anchored ~143 m ahead, y≈532) floats in-world as you approach. The `East Hargett Street` sign is ~141 m *behind* the spawn (y≈249) and ~167° off the north-facing axis, so press A or D to rotate around to it (W/S only translate, they never turn the camera). These names come from the road `name` tags, never invented. 4. **Street furniture.** Along the walk, yellow `T` traffic signals cluster at intersections, white `=` crosswalk bands cross the road, green `t` trees dot the verges, and a cyan `B` marks a bus stop. All are point records from the OSM furniture layer. 5. **Walk with a grade.** The HUD elevation climbs continuously from ~100.1 m to ~101.0 m over the ~120 m walk — `feet_z` follows terrain and movement is never rejected on this gentle grade. 6. **Solid footprint.** Turn toward the nearest building wall and hold W into it. Forward motion stops at the wall face; strafing (A/D while holding W) slides along it. You cannot pass through or under it. Re-run the validator after every fresh acquisition; if a data update moves a footprint onto the documented spawn, pick a new walkable spawn inside bounds and update `manifest.json` before publishing the pack. ## Known limitations of the committed sample - Terrain is bilinear interpolation of four EPQS corner observations — smooth and correct in trend, but it cannot show curb-level detail. A full pack should resample the 3DEP DEM at 2–5 m. - Most buildings use the `OSM building:levels × 3.2 m/floor` estimate (confidence 0.72); a minority carry an explicit OSM `height` tag (confidence 0.88). The 0.55/0.35 fallback tiers in `gis-ingestion.md` step 7 apply to lower-evidence cases than this sample contains. Skyline proportions are right; individual roof elevations may not be. - Buildings and surfaces crossing the bbox edge are clipped to the 630 × 770 m working extent and marked `clipped: true`; a small number that collapse below 3 vertices or self-intersect after clipping are dropped rather than weaken the validator's simple-polygon guarantee. - OSM does not record `highway=street_lamp` nodes for downtown Raleigh, so lamps are absent from props; road `lit` tags still drive ground brightness. Municipal furniture layers (signals, lamps, trees) on the City of Raleigh ArcGIS portal can fill this gap but were not required for the current census. - Raleigh municipal footprints are used as a geometry cross-check only; runtime footprints stay on ODbL-licensed OSM geometry so the sample remains redistributable with attribution.
-
-
scripts
-
validate-city-pack.py 15.3 KB
#!/usr/bin/env python3 """Validate an ASCII city pack using only the Python standard library.""" from __future__ import annotations import json import math import re import sys from datetime import date from pathlib import Path DATE_RE = re.compile(r"^\d{4}-\d{2}-\d{2}$") COLOR_RE = re.compile(r"^#[0-9a-fA-F]{6}$") # Resource caps: bound CPU/memory spent on any single pack so a crafted or # corrupt pack cannot trigger unbounded work in the O(n^2) geometry checks. MAX_POLYGON_VERTICES = 2000 # per building footprint or surface geometry MAX_ELEVATION_CELLS = 4_000_000 # per terrain grid (e.g. 2000x2000) MAX_FEATURES_PER_TILE = 50_000 # buildings + surfaces + props + signs combined # Documented per-kind prop glyph map (city-provider-contract.md). One glyph per kind. PROP_GLYPHS = {"traffic_signal": "T", "street_lamp": "i", "tree": "t", "bus_stop": "B", "bench": "b", "bollard": "o", "fire_hydrant": "f", "crossing": "="} FALLBACK_GLYPH = "?" class Report: def __init__(self): self.passed = 0; self.failed = 0 def rule(self, name, ok, detail=""): self.passed += bool(ok); self.failed += not ok suffix = f" — {detail}" if detail else "" print(f"{'PASS' if ok else 'FAIL'} {name}{suffix}") def load_json(path, report, label): try: data = json.loads(path.read_text(encoding="utf-8")) report.rule(label, True, str(path)) return data except Exception as exc: report.rule(label, False, f"{path}: {exc}") return None def valid_date(value): if not isinstance(value, str) or not DATE_RE.fullmatch(value): return False try: date.fromisoformat(value); return True except ValueError: return False def finite_number(value): return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value) def point(value): return isinstance(value, list) and len(value) == 2 and all(finite_number(v) for v in value) def orient(a,b,c): return (b[0]-a[0])*(c[1]-a[1])-(b[1]-a[1])*(c[0]-a[0]) def on_segment(a,b,p): return min(a[0],b[0]) <= p[0] <= max(a[0],b[0]) and min(a[1],b[1]) <= p[1] <= max(a[1],b[1]) def intersects(a,b,c,d): o1,o2,o3,o4 = orient(a,b,c),orient(a,b,d),orient(c,d,a),orient(c,d,b) if ((o1>0 and o2<0) or (o1<0 and o2>0)) and ((o3>0 and o4<0) or (o3<0 and o4>0)): return True return any(abs(o)<1e-9 and on_segment(x,y,p) for o,x,y,p in ((o1,a,b,c),(o2,a,b,d),(o3,c,d,a),(o4,c,d,b))) def simple_polygon(poly): if not isinstance(poly,list) or len(poly)<3 or not all(point(p) for p in poly): return False if len(poly) > MAX_POLYGON_VERTICES: return False pts = poly[:-1] if poly[0] == poly[-1] else poly if len(pts)<3 or len({tuple(p) for p in pts})<3 or abs(sum(pts[i][0]*pts[(i+1)%len(pts)][1]-pts[(i+1)%len(pts)][0]*pts[i][1] for i in range(len(pts))))<1e-9: return False n=len(pts) for i in range(n): for j in range(i+1,n): if j in (i,(i+1)%n) or i in (j,(j+1)%n): continue if i==0 and j==n-1: continue if intersects(pts[i],pts[(i+1)%n],pts[j],pts[(j+1)%n]): return False return True def valid_provenance(p, manifest=False): if not isinstance(p,dict): return False source_key = "name" if manifest else "source" required=(source_key,"url","license","retrieved") if not all(isinstance(p.get(k),str) and p[k] for k in required): return False if not p["url"].startswith("https://") or not valid_date(p["retrieved"]): return False if not manifest and not (finite_number(p.get("confidence")) and 0 <= p["confidence"] <= 1): return False if "confidence" in p and not (finite_number(p["confidence"]) and 0 <= p["confidence"] <= 1): return False return True def inside(bounds,x,y): return bounds["min_x"] <= x <= bounds["max_x"] and bounds["min_y"] <= y <= bounds["max_y"] def all_points(tile): for b in (tile.get("buildings") if isinstance(tile.get("buildings"),list) else []): if isinstance(b, dict): yield from b.get("footprint", []) for s in (tile.get("surfaces") if isinstance(tile.get("surfaces"),list) else []): if isinstance(s, dict): yield from s.get("geometry", []) for p in (tile.get("props") if isinstance(tile.get("props"),list) else []): if isinstance(p, dict): yield [p.get("x"), p.get("y")] for g in (tile.get("signs") if isinstance(tile.get("signs"),list) else []): if isinstance(g, dict): yield [g.get("x"), g.get("y")] def main(argv): report=Report(); pack=Path(argv[1]).resolve() if len(argv)==2 else None report.rule("pack.directory", bool(pack and pack.is_dir()), str(pack) if pack else "usage: validate-city-pack.py <pack-dir>") if not pack or not pack.is_dir(): return 1 root=Path(__file__).resolve().parents[1] ms=load_json(root/"templates/city-pack-manifest.schema.json",report,"schema.manifest.load") ws=load_json(root/"templates/world.schema.json",report,"schema.world.load") report.rule("schema.manifest.required-contract", bool(ms and set(("name","version","crs","bounds","tiles","provenance")) <= set(ms.get("required",[])))) report.rule("schema.world.required-contract", bool(ws and set(("terrain","buildings","surfaces","props")) <= set(ws.get("required",[])))) manifest=load_json(pack/"manifest.json",report,"manifest.parse") if not isinstance(manifest,dict): return 1 required=("name","version","crs","bounds","tiles","provenance") report.rule("manifest.required", all(k in manifest for k in required), ", ".join(required)) report.rule("manifest.identity", isinstance(manifest.get("name"),str) and bool(manifest["name"]) and isinstance(manifest.get("version"),str) and bool(re.fullmatch(r"\d+\.\d+\.\d+",manifest["version"])) and isinstance(manifest.get("crs"),str) and bool(manifest["crs"])) b=manifest.get("bounds") bounds_ok=isinstance(b,dict) and all(finite_number(b.get(k)) for k in ("min_x","min_y","max_x","max_y")) and b["min_x"]<b["max_x"] and b["min_y"]<b["max_y"] report.rule("manifest.bounds",bounds_ok) prov=manifest.get("provenance") report.rule("manifest.provenance",isinstance(prov,list) and bool(prov) and all(valid_provenance(p,True) for p in prov),"source URLs, licenses, ISO dates, confidence") tiles=manifest.get("tiles") report.rule("manifest.tiles.nonempty",isinstance(tiles,list) and bool(tiles) and all(isinstance(t,str) and t for t in tiles or [])) if not isinstance(tiles,list): tiles=[] tile_data=[] for idx,rel in enumerate(tiles): safe=isinstance(rel,str) and not Path(rel).is_absolute() path=(pack/rel).resolve() if safe else pack safe=safe and (pack==path or pack in path.parents) report.rule(f"tile[{idx}].path",safe and path.is_file(),str(rel)) data=load_json(path,report,f"tile[{idx}].parse") if safe and path.is_file() else None if isinstance(data,dict): tile_data.append((idx,rel,data)) # pack-wide road-name set so a sign in one tile may reference a road whose # name-carrying surface lives in another tile (the contract supports multi-tile) pack_road_names=set() for _idx,_rel,_tile in tile_data: for _s in (_tile.get("surfaces",[]) if isinstance(_tile.get("surfaces"),list) else []): if isinstance(_s,dict) and isinstance(_s.get("name"),str) and _s.get("name"): pack_road_names.add(_s["name"]) building_ids=[]; surface_ids=[]; building_count=0; surface_count=0; terrain_extents=[] for idx,rel,tile in tile_data: report.rule(f"tile[{idx}].required",all(k in tile for k in ("terrain","buildings","surfaces","props")),rel) n_b=len(tile.get("buildings",[])) if isinstance(tile.get("buildings"),list) else 0 n_s=len(tile.get("surfaces",[])) if isinstance(tile.get("surfaces"),list) else 0 n_p=len(tile.get("props",[])) if isinstance(tile.get("props"),list) else 0 n_g=len(tile.get("signs",[])) if isinstance(tile.get("signs"),list) else 0 total_features=n_b+n_s+n_p+n_g report.rule(f"tile[{idx}].feature-count",total_features<=MAX_FEATURES_PER_TILE,f"{total_features} features") if total_features>MAX_FEATURES_PER_TILE: # still collect IDs for pack-wide uniqueness even though we skip the # expensive per-feature geometry checks (so duplicates in an oversized # tile are not silently accepted) for _j,it in enumerate(tile.get("buildings",[]) if isinstance(tile.get("buildings"),list) else []): if isinstance(it,dict) and isinstance(it.get("id"),str): building_ids.append(it["id"]) for _j,it in enumerate(tile.get("surfaces",[]) if isinstance(tile.get("surfaces"),list) else []): if isinstance(it,dict) and isinstance(it.get("id"),str): surface_ids.append(it["id"]) continue # short-circuit quadratic work below terrain=tile.get("terrain",{}); elev=terrain.get("elevations"); res=terrain.get("resolution_m"); origin=terrain.get("origin") rectangular=isinstance(elev,list) and len(elev)>=2 and all(isinstance(row,list) and len(row)>=2 for row in elev) and len({len(row) for row in elev})==1 and all(v is None or finite_number(v) for row in elev for v in row) cells=sum(len(row) for row in elev) if isinstance(elev,list) else 0 report.rule(f"tile[{idx}].terrain.cells",cells<=MAX_ELEVATION_CELLS,f"{cells} cells") terrain_ok=finite_number(res) and res>0 and point(origin) and rectangular and cells<=MAX_ELEVATION_CELLS and valid_provenance(terrain.get("provenance")) report.rule(f"tile[{idx}].terrain",terrain_ok,"rectangular grid, meter resolution, provenance") if terrain_ok: ext=(origin[0],origin[1],origin[0]+(len(elev[0])-1)*res,origin[1]+(len(elev)-1)*res); terrain_extents.append(ext) report.rule(f"tile[{idx}].terrain.bounds",bool(bounds_ok and inside(b,ext[0],ext[1]) and inside(b,ext[2],ext[3])),str(ext)) buildings=tile.get("buildings",[]); building_count += len(buildings) if isinstance(buildings,list) else 0 b_ok=isinstance(buildings,list) for j,item in enumerate(buildings if isinstance(buildings,list) else []): ok=isinstance(item,dict) and isinstance(item.get("id"),str) and bool(item["id"]) and simple_polygon(item.get("footprint")) and finite_number(item.get("base_elev_m")) and finite_number(item.get("height_m")) and item["height_m"]>0 and isinstance(item.get("color"),str) and bool(COLOR_RE.fullmatch(item["color"])) and valid_provenance(item.get("provenance")) report.rule(f"tile[{idx}].building[{j}]",ok,item.get("id","missing id") if isinstance(item,dict) else "not object"); b_ok &= ok if isinstance(item,dict) and isinstance(item.get("id"),str): building_ids.append(item["id"]) report.rule(f"tile[{idx}].buildings",b_ok,f"count={len(buildings) if isinstance(buildings,list) else 0}") surfaces=tile.get("surfaces",[]); surface_count += len(surfaces) if isinstance(surfaces,list) else 0 s_ok=isinstance(surfaces,list) for j,item in enumerate(surfaces if isinstance(surfaces,list) else []): ok=isinstance(item,dict) and isinstance(item.get("id"),str) and bool(item["id"]) and isinstance(item.get("kind"),str) and bool(item["kind"]) and isinstance(item.get("walkable"),bool) and isinstance(item.get("geometry"),list) and 2<=len(item["geometry"])<=MAX_POLYGON_VERTICES and all(point(p) for p in item["geometry"]) and valid_provenance(item.get("provenance")) report.rule(f"tile[{idx}].surface[{j}]",ok,item.get("id","missing id") if isinstance(item,dict) else "not object"); s_ok &= ok if isinstance(item,dict) and isinstance(item.get("id"),str): surface_ids.append(item["id"]) report.rule(f"tile[{idx}].surfaces",s_ok,f"count={len(surfaces) if isinstance(surfaces,list) else 0}") props=tile.get("props",[]) p_ok=isinstance(props,list) for j,item in enumerate(props if isinstance(props,list) else []): ok=isinstance(item,dict) and isinstance(item.get("id"),str) and bool(item["id"]) and isinstance(item.get("kind"),str) and bool(item["kind"]) and finite_number(item.get("x")) and finite_number(item.get("y")) if ok and "provenance" in item: ok = ok and valid_provenance(item.get("provenance")) report.rule(f"tile[{idx}].prop[{j}]",ok,f"{item.get('kind','?')} ({item.get('id','?')})" if isinstance(item,dict) else "not object"); p_ok &= ok known=sorted({p["kind"] for p in (props if isinstance(props,list) else []) if isinstance(p,dict) and isinstance(p.get("kind"),str) and p["kind"] in PROP_GLYPHS}) unknown=sorted({p["kind"] for p in (props if isinstance(props,list) else []) if isinstance(p,dict) and isinstance(p.get("kind"),str) and p["kind"] not in PROP_GLYPHS}) report.rule(f"tile[{idx}].props",p_ok,f"count={len(props) if isinstance(props,list) else 0} kinds={len(known)}") # unknown kinds are permitted: the engine renders them with the documented # fallback glyph '?'. Report them (so a misspelled kind is visible) but do not # fail the pack on their presence. if unknown: report.rule(f"tile[{idx}].props.unknown-kinds",True,f"rendered with fallback '{FALLBACK_GLYPH}': {', '.join(unknown)}") signs=tile.get("signs",[]) g_ok=isinstance(signs,list) for j,item in enumerate(signs if isinstance(signs,list) else []): ok=isinstance(item,dict) and isinstance(item.get("id"),str) and bool(item["id"]) and isinstance(item.get("text"),str) and bool(item["text"]) and item["text"] in pack_road_names and finite_number(item.get("x")) and finite_number(item.get("y")) if ok and "provenance" in item: ok = ok and valid_provenance(item.get("provenance")) report.rule(f"tile[{idx}].sign[{j}]",ok,f"{item.get('text','?')} ({item.get('id','?')})" if isinstance(item,dict) else "not object"); g_ok &= ok # emit the signs rule unconditionally: a non-list/null signs value must FAIL, # matching how props is reported regardless of type signs_ok = isinstance(signs,list) if signs_ok: signs_ok = g_ok report.rule(f"tile[{idx}].signs",signs_ok,f"count={len(signs) if isinstance(signs,list) else 0}") extents_ok=bool(bounds_ok) and all(point(p) and inside(b,p[0],p[1]) for p in all_points(tile)) report.rule(f"tile[{idx}].content.bounds",extents_ok,rel) from collections import Counter bc=Counter(building_ids); sc=Counter(surface_ids) duplicates=sorted(x for x,n in bc.items() if n>1) report.rule("buildings.unique-ids",not duplicates,", ".join(duplicates)) surface_dupes=sorted(x for x,n in sc.items() if n>1) report.rule("surfaces.unique-ids",not surface_dupes,", ".join(surface_dupes)) if isinstance(manifest.get("spawn"),dict) and bounds_ok: s=manifest["spawn"]; report.rule("manifest.spawn.bounds",finite_number(s.get("x")) and finite_number(s.get("y")) and finite_number(s.get("heading_deg")) and inside(b,s["x"],s["y"])) if terrain_extents: minx=min(e[0] for e in terrain_extents); miny=min(e[1] for e in terrain_extents); maxx=max(e[2] for e in terrain_extents); maxy=max(e[3] for e in terrain_extents) extent=f"[{minx:.1f}, {miny:.1f}]..[{maxx:.1f}, {maxy:.1f}]" else: extent="none" print(f"SUMMARY rules_passed={report.passed} rules_failed={report.failed} buildings={building_count} terrain_extent={extent} surfaces={surface_count}") return 0 if report.failed==0 else 1 if __name__ == "__main__": raise SystemExit(main(sys.argv))
-
-
templates
-
city-pack-manifest.schema.json 1.7 KB
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "city-pack-manifest.schema.json", "title": "ASCII city pack manifest", "type": "object", "required": ["name", "version", "crs", "bounds", "tiles", "provenance"], "properties": { "name": {"type": "string", "minLength": 1}, "version": {"type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$"}, "crs": {"type": "string", "minLength": 1}, "bounds": { "type": "object", "required": ["min_x", "min_y", "max_x", "max_y"], "properties": { "min_x": {"type": "number"}, "min_y": {"type": "number"}, "max_x": {"type": "number"}, "max_y": {"type": "number"} }, "additionalProperties": false }, "tiles": {"type": "array", "minItems": 1, "items": {"type": "string", "minLength": 1}}, "spawn": { "type": "object", "required": ["x", "y", "heading_deg"], "properties": {"x": {"type": "number"}, "y": {"type": "number"}, "heading_deg": {"type": "number"}}, "additionalProperties": false }, "provenance": { "type": "array", "minItems": 1, "items": { "type": "object", "required": ["name", "url", "license", "retrieved"], "properties": { "name": {"type": "string", "minLength": 1}, "url": {"type": "string", "pattern": "^https://"}, "license": {"type": "string", "minLength": 1}, "retrieved": {"type": "string", "format": "date"}, "confidence": {"type": "number", "minimum": 0, "maximum": 1}, "notes": {"type": "string"} }, "additionalProperties": true } }, "extensions": {"type": "object"} }, "additionalProperties": false } -
world.schema.json 3.4 KB
{ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "world.schema.json", "title": "ASCII city world tile", "type": "object", "required": ["terrain", "buildings", "surfaces", "props"], "$defs": { "point": {"type": "array", "prefixItems": [{"type": "number"}, {"type": "number"}], "minItems": 2, "maxItems": 2}, "provenance": { "type": "object", "required": ["source", "url", "license", "retrieved", "confidence"], "properties": { "source": {"type": "string", "minLength": 1}, "url": {"type": "string", "pattern": "^https://"}, "license": {"type": "string", "minLength": 1}, "retrieved": {"type": "string", "format": "date"}, "confidence": {"type": "number", "minimum": 0, "maximum": 1}, "method": {"type": "string"}, "clipped": {"type": "boolean"} }, "additionalProperties": true } }, "properties": { "terrain": { "type": "object", "required": ["resolution_m", "origin", "elevations", "provenance"], "properties": { "resolution_m": {"type": "number", "exclusiveMinimum": 0}, "origin": {"$ref": "#/$defs/point"}, "elevations": {"type": "array", "minItems": 2, "items": {"type": "array", "minItems": 2, "items": {"type": ["number", "null"]}}}, "provenance": {"$ref": "#/$defs/provenance"} }, "additionalProperties": false }, "buildings": { "type": "array", "items": { "type": "object", "required": ["id", "footprint", "base_elev_m", "height_m", "color", "provenance"], "properties": { "id": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "footprint": {"type": "array", "minItems": 3, "items": {"$ref": "#/$defs/point"}}, "base_elev_m": {"type": "number"}, "height_m": {"type": "number", "exclusiveMinimum": 0}, "color": {"type": "string", "pattern": "^#[0-9A-Fa-f]{6}$"}, "provenance": {"$ref": "#/$defs/provenance"} }, "additionalProperties": true } }, "surfaces": { "type": "array", "items": { "type": "object", "required": ["id", "kind", "geometry", "walkable", "provenance"], "properties": { "id": {"type": "string", "minLength": 1}, "kind": {"type": "string", "minLength": 1}, "name": {"type": "string"}, "geometry": {"type": "array", "minItems": 2, "items": {"$ref": "#/$defs/point"}}, "walkable": {"type": "boolean"}, "provenance": {"$ref": "#/$defs/provenance"} }, "additionalProperties": true } }, "props": { "type": "array", "items": { "type": "object", "required": ["id", "kind", "x", "y"], "properties": {"id": {"type": "string", "minLength": 1}, "kind": {"type": "string", "minLength": 1}, "x": {"type": "number"}, "y": {"type": "number"}, "label": {"type": "string"}, "provenance": {"$ref": "#/$defs/provenance"}}, "additionalProperties": true } }, "signs": { "type": "array", "items": { "type": "object", "required": ["id", "text", "x", "y"], "properties": {"id": {"type": "string", "minLength": 1}, "text": {"type": "string", "minLength": 1}, "x": {"type": "number"}, "y": {"type": "number"}, "anchor_way": {"type": "string"}, "provenance": {"$ref": "#/$defs/provenance"}}, "additionalProperties": true } }, "extensions": {"type": "object"} }, "additionalProperties": false }
-
-
README.md 2 KB
# 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 ```sh 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. -
SKILL.md 4.3 KB
--- name: ascii-city-engine description: Build portable, first-person colored ASCII city engines and small GIS-derived city packs. Use when designing terrain-following walking, raycast character rendering, city-provider schemas, or reproducible public-GIS ingestion. Do not use for conventional 3D/WebGL games, multi-level interiors, general GIS analysis, or committing full-resolution GIS archives. --- # ASCII City Engine Build a portable city experience in three layers: an engine, a city-provider contract, and a city pack. Keep city-specific facts out of engine code. ## Workflow 1. Define the pack boundary and local meter-based CRS. 2. Acquire elevation, building, and road/path data from public sources. 3. Verify downloads, licenses, units, and provenance before conversion. 4. Reproject all geometry to local meters and emit the manifest plus world tiles. 5. Validate the pack offline: ```sh python3 scripts/validate-city-pack.py path/to/city-pack ``` 6. Load the pack's first tile in `assets/ascii-city-engine.html` and test movement, grade following, and solid footprints. 7. Record data limitations and human acceptance evidence. ## Required invariants - Compute `feet_z` from terrain and `eye_z` from feet plus eye height. - Reject null terrain, over-steep steps, and movement touching a solid footprint. - Use deterministic building colors and clear missed rays every frame. - Treat v1 as one ground height per `(x, y)` column; reserve, but do not implement, a surface graph. - Keep the engine independent of any city, vendor, agent harness, or private infrastructure. - Do not commit large or full-resolution source data. Commit only a small redistributable sample; document acquisition for the rest. ## Reference routing - Read [references/engine-architecture.md](references/engine-architecture.md) for world math, rendering, collisions, and the scaffold contract. - Read [references/city-provider-contract.md](references/city-provider-contract.md) when creating or validating a provider. - Read [references/gis-ingestion.md](references/gis-ingestion.md) before downloading or converting GIS sources. - Read [references/raleigh-poc.md](references/raleigh-poc.md) for the reproducible Raleigh proof of concept. - Use [templates/city-pack-manifest.schema.json](templates/city-pack-manifest.schema.json) and [templates/world.schema.json](templates/world.schema.json) as authoritative data contracts. ## Available Scripts This skill bundles one script; there are no others to discover. | Script | Purpose | Invocation | |---|---|---| | `scripts/validate-city-pack.py` | Offline validator for a city pack: checks the manifest against the schema contract and verifies world tiles. Run it at workflow step 5, after emitting the manifest and tiles and before loading anything in the engine — do not proceed to in-engine testing until it passes. | `python3 scripts/validate-city-pack.py path/to/city-pack` | ## Prerequisites - Python 3 with standard library only; the validator requires no third-party packages. - A city pack produced through the ingestion workflow: manifest plus world tiles reprojected to local meters (see [references/gis-ingestion.md](references/gis-ingestion.md)). - The engine itself is a standalone HTML file (`assets/ascii-city-engine.html`) that runs in any modern browser; no build step or server is required. ## Limitations - v1 supports one ground height per `(x, y)` column: no interiors, bridges with traversable space below, tunnels, or multi-level geometry (a surface graph is reserved but not implemented). - The engine is software-rendered CPU canvas — no WebGL/GPU rendering or mobile controls. - The skill does not include full-resolution GIS archives; packs are built reproducibly from public sources, committing only small redistributable samples. - The validator is offline and structural: passing it does not substitute for human movement testing in the engine (workflow step 6). ## Boundaries Use a different skill or implementation approach for WebGL/GPU rendering, mobile controls, combat, interiors, bridges with traversable space below, tunnels, or general-purpose geospatial analysis. If asked to specialize this skill for one agent runtime, preserve the portable core and put runtime integration outside this skill. If asked to commit large GIS binaries, refuse and provide reproducible download/conversion commands instead.
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.