feat(climate-sim): Update StatsDashboard component with enhanced visualizations and improve sprite generation tooling for better asset handling

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-26 01:19:10 -07:00
parent e1880d53b1
commit 670cc24d7a
6 changed files with 55 additions and 41 deletions

View file

@ -52,6 +52,39 @@ interface MetricDef {
phaseBands?: PhaseBand[]
}
// ── Formatters ───────────────────────────────────────────────────────────────
const fmt3 = (v: number) => v.toFixed(3)
const fmt2 = (v: number) => v.toFixed(2)
const fmt4 = (v: number) => v.toFixed(4)
const fmtDelta3 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(3)
const fmtDelta2 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(2)
const fmtDelta4 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(4)
// ── Metric catalog (single source of truth) ─────────────────────────────────
const METRIC_CATALOG: Record<string, MetricDef> = {
// -- Climate / atmosphere --
sea_level: { key: 'sea_level', label: 'Sea Lvl', tooltip: 'Elevation threshold for water. Rises with warming, falls with cooling.', color: '#5C6BC0', getValue: (s) => s.sea_level, formatValue: fmt3, formatDelta: fmtDelta3 },
et: { key: 'evapotrans', label: 'ET', tooltip: 'Average evapotranspiration across land. Moisture recycled by vegetation per turn.', color: '#80DEEA', getValue: (s) => s.avg_evapotranspiration, formatValue: fmt4, formatDelta: fmtDelta4 },
albedo: { key: 'albedo', label: 'Albedo', tooltip: 'Global average surface reflectivity (0=absorbs all, 1=reflects all). Ice/snow raise it, forests/water lower it.', color: '#BDBDBD', getValue: (s) => s.avg_albedo, formatValue: fmt3, formatDelta: fmtDelta3 },
aerosol: { key: 'aerosol', label: 'Aerosol', tooltip: 'Global average sulfate aerosol opacity. Volcanic eruptions inject aerosols that cool and dry the atmosphere.', color: '#90A4AE', getValue: (s) => s.avg_aerosol, formatValue: fmt4, formatDelta: fmtDelta4 },
// -- Land ecology --
land_canopy: { key: 'land_canopy', label: 'Flora', tooltip: 'Average tree canopy cover across land (0=barren, 1=full canopy). Drives succession and shading.', color: '#2E7D32', getValue: (s) => s.avg_land_canopy, formatValue: fmt3, formatDelta: fmtDelta3 },
land_under: { key: 'land_undergrowth', label: 'Undergrowth', tooltip: 'Average ground vegetation across land (0=bare, 1=dense). Drives food yield and habitat quality.', color: '#66BB6A', getValue: (s) => s.avg_land_undergrowth, formatValue: fmt3, formatDelta: fmtDelta3 },
land_fungi: { key: 'land_fungi', label: 'Fungi', tooltip: 'Average mycorrhizal network across land (0=none, 1=dense). Accelerates forest regrowth, boosts ecosystem resilience.', color: '#8D6E63', getValue: (s) => s.avg_land_fungi, formatValue: fmt3, formatDelta: fmtDelta3 },
land_habitat: { key: 'land_habitat', label: 'Fauna', tooltip: 'Average fauna habitat suitability across land (0=inhospitable, 1=thriving). Combines flora density, moisture, temperature.', color: '#A1887F', getValue: (s) => s.avg_land_habitat, formatValue: fmt3, formatDelta: fmtDelta3 },
land_quality: { key: 'land_quality', label: 'Quality', tooltip: 'Average tile quality across land (1=Prolific, 5=Epic). Ecology composite of flora health, fauna diversity, biome stability.', color: '#FFD54F', getValue: (s) => s.avg_land_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
// -- Water ecology --
water_reef: { key: 'water_reef', label: 'Flora', tooltip: 'Average reef health across water tiles (0=dead, 1=pristine). Bleaching from high temps (>0.75). Dead reefs halve fish capacity.', color: '#29B6F6', getValue: (s) => s.avg_water_reef, formatValue: fmt3, formatDelta: fmtDelta3 },
water_fish: { key: 'water_fish', label: 'Fauna', tooltip: 'Average fish stock across water tiles (0=empty, 100+=abundant). Logistic reproduction, temperature-scaled.', color: '#26C6DA', getValue: (s) => s.avg_water_fish, formatValue: fmt2, formatDelta: fmtDelta2 },
water_quality:{ key: 'water_quality', label: 'Quality', tooltip: 'Average tile quality across water tiles (1=Prolific, 5=Epic). Reflects marine ecosystem health.', color: '#42A5F5', getValue: (s) => s.avg_water_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
}
const m = (key: string): MetricDef => METRIC_CATALOG[key]
// ── Layout selections ────────────────────────────────────────────────────────
const PRIMARY_METRICS: MetricDef[] = [
{
key: 'temp', label: 'Temp', tooltip: 'Average land temperature (0=frozen, 1=scorching). Driven by solar input, albedo, and orbital cycles.', color: '#E85D3A',
@ -69,44 +102,14 @@ const PRIMARY_METRICS: MetricDef[] = [
m('sea_level'),
]
// ── Metric catalog (single source of truth) ─────────────────────────────────
// Each metric defined ONCE. Dashboard layouts select by key.
const fmt3 = (v: number) => v.toFixed(3)
const fmt2 = (v: number) => v.toFixed(2)
const fmt4 = (v: number) => v.toFixed(4)
const fmtDelta3 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(3)
const fmtDelta2 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(2)
const fmtDelta4 = (d: number) => (d >= 0 ? '+' : '') + d.toFixed(4)
const METRIC_CATALOG: Record<string, MetricDef> = {
// -- Climate / atmosphere --
sea_level: { key: 'sea_level', label: 'Sea Lvl', tooltip: 'Elevation threshold for water. Rises with warming, falls with cooling.', color: '#5C6BC0', getValue: (s) => s.sea_level, formatValue: fmt3, formatDelta: fmtDelta3 },
et: { key: 'evapotrans', label: 'ET', tooltip: 'Average evapotranspiration across land. Moisture recycled by vegetation per turn.', color: '#80DEEA', getValue: (s) => s.avg_evapotranspiration, formatValue: fmt4, formatDelta: fmtDelta4 },
albedo: { key: 'albedo', label: 'Albedo', tooltip: 'Global average surface reflectivity (0=absorbs all, 1=reflects all). Ice/snow raise it, forests/water lower it.', color: '#BDBDBD', getValue: (s) => s.avg_albedo, formatValue: fmt3, formatDelta: fmtDelta3 },
aerosol: { key: 'aerosol', label: 'Aerosol', tooltip: 'Global average sulfate aerosol opacity. Volcanic eruptions inject aerosols that cool and dry the atmosphere.', color: '#90A4AE', getValue: (s) => s.avg_aerosol, formatValue: fmt4, formatDelta: fmtDelta4 },
// -- Land ecology --
land_canopy: { key: 'land_canopy', label: 'Canopy', tooltip: 'Average tree canopy cover across land (0=barren, 1=full canopy). Drives succession and shading.', color: '#2E7D32', getValue: (s) => s.avg_land_canopy, formatValue: fmt3, formatDelta: fmtDelta3 },
land_under: { key: 'land_undergrowth', label: 'Undergrowth', tooltip: 'Average ground vegetation across land (0=bare, 1=dense). Drives food yield and habitat quality.', color: '#66BB6A', getValue: (s) => s.avg_land_undergrowth, formatValue: fmt3, formatDelta: fmtDelta3 },
land_fungi: { key: 'land_fungi', label: 'Fungi', tooltip: 'Average mycorrhizal network across land (0=none, 1=dense). Accelerates forest regrowth, boosts ecosystem resilience.', color: '#8D6E63', getValue: (s) => s.avg_land_fungi, formatValue: fmt3, formatDelta: fmtDelta3 },
land_habitat: { key: 'land_habitat', label: 'Habitat', tooltip: 'Average habitat suitability across land (0=inhospitable, 1=thriving). Combines flora density, moisture, temperature.', color: '#A1887F', getValue: (s) => s.avg_land_habitat, formatValue: fmt3, formatDelta: fmtDelta3 },
land_quality: { key: 'land_quality', label: 'Quality', tooltip: 'Average tile quality across land (1=Prolific, 5=Epic). Ecology composite of flora health, fauna diversity, biome stability.', color: '#FFD54F', getValue: (s) => s.avg_land_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
// -- Water ecology --
water_reef: { key: 'water_reef', label: 'Reef', tooltip: 'Average reef health across water tiles (0=dead, 1=pristine). Bleaching from high temps (>0.75). Dead reefs halve fish capacity.', color: '#29B6F6', getValue: (s) => s.avg_water_reef, formatValue: fmt3, formatDelta: fmtDelta3 },
water_fish: { key: 'water_fish', label: 'Fish', tooltip: 'Average fish stock across water tiles (0=empty, 100+=abundant). Logistic reproduction, temperature-scaled.', color: '#26C6DA', getValue: (s) => s.avg_water_fish, formatValue: fmt2, formatDelta: fmtDelta2 },
water_quality:{ key: 'water_quality', label: 'Quality', tooltip: 'Average tile quality across water tiles (1=Prolific, 5=Epic). Reflects marine ecosystem health.', color: '#42A5F5', getValue: (s) => s.avg_water_quality, formatValue: fmt2, formatDelta: fmtDelta2 },
}
const m = (key: string): MetricDef => METRIC_CATALOG[key]
// Life mode: LAND column (fauna quality first, then flora quality, then details)
// Life mode: LAND column — Fauna, Flora, Quality, then details
const LIFE_LEFT: MetricDef[] = [
m('land_habitat'), m('land_quality'), m('land_canopy'), m('land_under'), m('land_fungi'),
m('land_habitat'), m('land_canopy'), m('land_quality'), m('land_under'), m('land_fungi'),
]
// Life mode: WATER column (fauna quality first, then flora quality, then details)
// Life mode: WATER column — Fauna, Flora, Quality
const LIFE_RIGHT: MetricDef[] = [
m('water_fish'), m('water_quality'), m('water_reef'),
m('water_fish'), m('water_reef'), m('water_quality'),
]
// Environment mode: left column (energy budget)
@ -359,7 +362,9 @@ function TerrainDistChart({ stats, currentTurn, onScrub }: StatsDashboardProps):
// Match total height of primary + compact sections
// Height matches the tallest possible content (life mode with headers)
const maxCompactRows = Math.max(LIFE_LEFT.length, LIFE_RIGHT.length) + 1
const lifeRows = Math.max(LIFE_LEFT.length, LIFE_RIGHT.length) + 1 // +1 for header
const envRows = Math.max(ENV_LEFT.length, ENV_RIGHT.length)
const maxCompactRows = Math.max(lifeRows, envRows)
const chartH = PRIMARY_METRICS.length * (PRIMARY_H + 3)
+ 6
+ maxCompactRows * (COMPACT_H + 3)

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View file

@ -189,22 +189,31 @@ def create_app(
@app.get("/api/stream/variants")
async def stream_variants() -> StreamingResponse:
async def event_generator():
# Start from the highest known variant ID
current = registry.get_recent_variants(limit=1)
last_id = current[0]["variant_id"] if current else 0
last_rating_snapshot = ""
poll_count = 0
while True:
await asyncio.sleep(3)
new_variants = registry.get_recent_variants(limit=10, since_id=last_id)
if new_variants:
last_id = max(v["variant_id"] for v in new_variants)
yield f"data: {json.dumps(new_variants)}\n\n"
else:
# Also check for rating updates on recent variants
continue
# Check for rating updates every ~15s (5 cycles)
poll_count += 1
if poll_count % 5 == 0:
updated = registry.get_recent_variants(limit=30)
if updated:
snapshot = "|".join(
f"{v['variant_id']}:{v['rating']}" for v in updated
)
if snapshot != last_rating_snapshot:
last_rating_snapshot = snapshot
yield f"data: {json.dumps(updated)}\n\n"
else:
yield ": keepalive\n\n"
continue
yield ": keepalive\n\n"
return StreamingResponse(
event_generator(),

Binary file not shown.