Spaces:
Sleeping
Sleeping
| // normalizeEpisode.js | |
| // ---------------------------------------------------------------------------- | |
| // Bridges two episode JSON shapes: | |
| // | |
| // 1. The synthetic shape produced by `src/sampleRun.js` (rich; includes | |
| // thresholds, event_multipliers, treasury_before/after, debate/votes…). | |
| // | |
| // 2. The lean shape that `core/game.py::StepResult.to_dict()` actually | |
| // emits today (single `treasury`, only `demands` per sector, no | |
| // orchestrator-level data, etc.). | |
| // | |
| // `normalizeEpisode()` returns a uniform shape that every visualizer | |
| // component can rely on, deriving anything that's missing from the spec | |
| // formulas in `specification/04_ECONOMY_MODEL.md`. | |
| // ---------------------------------------------------------------------------- | |
| const CRITICAL_RATIO = 0.4; | |
| const SURPLUS_RATIO = 1.5; | |
| const WASTAGE_RATIO = 2.5; | |
| function thresholdsFromDemand(demand) { | |
| return { | |
| critical: demand * CRITICAL_RATIO, | |
| demand, | |
| surplus: demand * SURPLUS_RATIO, | |
| wastage: demand * WASTAGE_RATIO, | |
| }; | |
| } | |
| function aggregateEventMultipliers(events, sectorNames) { | |
| const out = Object.fromEntries(sectorNames.map(n => [n, 1])); | |
| for (const ev of events || []) { | |
| const aff = ev.affected_sectors || ev.affected_departments || {}; | |
| if (Array.isArray(aff)) continue; | |
| for (const [name, mult] of Object.entries(aff)) { | |
| if (out[name] !== undefined) out[name] *= Number(mult) || 1; | |
| } | |
| } | |
| return out; | |
| } | |
| function deriveThresholds(round, sectorNames) { | |
| const existing = round.thresholds || {}; | |
| const demands = round.demands || {}; | |
| const out = {}; | |
| for (const name of sectorNames) { | |
| if (existing[name] && existing[name].demand !== undefined) { | |
| const t = existing[name]; | |
| out[name] = { | |
| critical: t.critical ?? t.demand * CRITICAL_RATIO, | |
| demand: t.demand, | |
| surplus: t.surplus ?? t.demand * SURPLUS_RATIO, | |
| wastage: t.wastage ?? t.demand * WASTAGE_RATIO, | |
| }; | |
| } else if (demands[name] !== undefined) { | |
| out[name] = thresholdsFromDemand(Number(demands[name]) || 0); | |
| } else { | |
| out[name] = thresholdsFromDemand(0); | |
| } | |
| } | |
| return out; | |
| } | |
| function average(arr) { | |
| if (!arr.length) return 0; | |
| return arr.reduce((a, b) => a + b, 0) / arr.length; | |
| } | |
| function isPositiveEvent(ev) { | |
| if (typeof ev.is_positive === 'boolean') return ev.is_positive; | |
| if (Number(ev.treasury_injection) > 0) return true; | |
| if (typeof ev.category === 'string') return ev.category === 'positive'; | |
| const aff = ev.affected_sectors || {}; | |
| const mults = Object.values(aff).map(Number).filter(Number.isFinite); | |
| if (mults.length === 0) return false; | |
| return mults.every(m => m < 1); | |
| } | |
| function findFailingSectors(allocations, thresholds, sectorNames) { | |
| const failing = []; | |
| for (const name of sectorNames) { | |
| const a = Number(allocations[name] ?? 0); | |
| const t = thresholds[name] || {}; | |
| if (a < (t.critical ?? Infinity)) failing.push(name); | |
| } | |
| return failing; | |
| } | |
| export function normalizeEpisode(raw) { | |
| const safe = raw || {}; | |
| const rawRounds = Array.isArray(safe.rounds) ? safe.rounds : []; | |
| const sectors = safe.config?.sectors || []; | |
| const sectorNames = sectors.map(s => s.name); | |
| let cum = 0; | |
| let prevTreasuryAfter = safe.config?.initial_treasury ?? 1000; | |
| const rounds = rawRounds.map((r, i) => { | |
| const events = (r.events || []).map(ev => ({ | |
| ...ev, | |
| affected_sectors: ev.affected_sectors || {}, | |
| severity: ev.severity ?? 1, | |
| is_positive: isPositiveEvent(ev), | |
| treasury_injection: Number(ev.treasury_injection) || 0, | |
| })); | |
| const allocations = r.allocations || {}; | |
| const revenues = r.revenues || {}; | |
| const revenueFactors = r.revenue_factors || {}; | |
| const consumptions = r.consumptions || {}; | |
| const thresholds = deriveThresholds(r, sectorNames); | |
| const eventMultipliers = | |
| r.event_multipliers || aggregateEventMultipliers(events, sectorNames); | |
| const totalAllocation = | |
| r.total_allocation ?? | |
| sectorNames.reduce((s, n) => s + (Number(allocations[n]) || 0), 0); | |
| const totalRevenue = | |
| r.total_revenue ?? | |
| sectorNames.reduce((s, n) => s + (Number(revenues[n]) || 0), 0); | |
| const surplusReturned = | |
| r.surplus_returned ?? | |
| sectorNames.reduce( | |
| (s, n) => | |
| s + Math.max(0, (Number(allocations[n]) || 0) - (Number(consumptions[n]) || 0)), | |
| 0, | |
| ); | |
| const treasuryAfter = r.treasury_after ?? r.treasury ?? prevTreasuryAfter; | |
| const treasuryBefore = r.treasury_before ?? prevTreasuryAfter; | |
| prevTreasuryAfter = treasuryAfter; | |
| const treasuryInjection = | |
| r.treasury_injection ?? | |
| events.reduce((s, e) => s + (Number(e.treasury_injection) || 0), 0); | |
| const population = Number(r.population) || 0; | |
| const productivity = Number(r.productivity) || 1; | |
| const avgRf = | |
| r.avg_revenue_factor ?? | |
| average(sectorNames.map(n => Number(revenueFactors[n]) || 0)); | |
| const prosperity = | |
| r.prosperity ?? (population > 0 ? totalRevenue / population : 0); | |
| const reward = r.reward || {}; | |
| const total = Number(reward.total ?? 0); | |
| cum += total; | |
| const cumulative = r.cumulative_reward ?? cum; | |
| const isCriticalFailure = Boolean( | |
| r.done && | |
| typeof r.termination_reason === 'string' && | |
| r.termination_reason.startsWith('CRITICAL_FAILURE'), | |
| ); | |
| const failingSectors = isCriticalFailure | |
| ? findFailingSectors(allocations, thresholds, sectorNames) | |
| : []; | |
| const roundNum = r.round_num ?? i + 1; | |
| return { | |
| round_num: roundNum, | |
| year: r.year ?? Math.floor((roundNum - 1) / 4) + 1, | |
| quarter: r.quarter ?? ((roundNum - 1) % 4) + 1, | |
| events, | |
| crisis_occurred: r.crisis_occurred ?? events.some(e => (e.severity || 0) >= 4), | |
| treasury_injection: treasuryInjection, | |
| debate: r.debate || [], | |
| proposal_order: r.proposal_order || sectorNames, | |
| proposals: r.proposals || [], | |
| votes: r.votes || [], | |
| vote_results: r.vote_results || [], | |
| allocations, | |
| consumptions, | |
| revenues, | |
| revenue_factors: revenueFactors, | |
| thresholds, | |
| event_multipliers: eventMultipliers, | |
| treasury_before: treasuryBefore, | |
| treasury_after: treasuryAfter, | |
| total_allocation: totalAllocation, | |
| total_revenue: totalRevenue, | |
| surplus_returned: surplusReturned, | |
| population, | |
| productivity, | |
| avg_revenue_factor: avgRf, | |
| prosperity, | |
| reward: { | |
| base_reward: Number(reward.base_reward) || 0, | |
| productivity_bonus: Number(reward.productivity_bonus) || 0, | |
| survival_bonus: Number(reward.survival_bonus) || 0, | |
| over_alloc_penalty: Number(reward.over_alloc_penalty) || 0, | |
| under_alloc_penalty: Number(reward.under_alloc_penalty) || 0, | |
| critical_penalty: Number(reward.critical_penalty) || 0, | |
| total, | |
| }, | |
| cumulative_reward: cumulative, | |
| done: Boolean(r.done), | |
| termination_reason: r.termination_reason ?? null, | |
| is_critical_failure: isCriticalFailure, | |
| failing_sectors: failingSectors, | |
| }; | |
| }); | |
| const last = rounds.length > 0 ? rounds[rounds.length - 1] : null; | |
| let summary = safe.summary || null; | |
| if (!summary && last) { | |
| const finalProsperity = | |
| (last.treasury_after + last.total_revenue) / | |
| Math.max(last.population, 1); | |
| summary = { | |
| rounds_survived: last.round_num, | |
| total_reward: cum, | |
| final_treasury: last.treasury_after, | |
| final_prosperity: finalProsperity, | |
| final_productivity: last.productivity, | |
| final_population: last.population, | |
| termination_reason: last.termination_reason, | |
| }; | |
| } | |
| return { | |
| ...safe, | |
| config: { | |
| ...(safe.config || {}), | |
| sectors, | |
| }, | |
| rounds, | |
| summary, | |
| }; | |
| } | |