// game-levels.jsx — Niveles 1-4: educational levels that build understanding // before the final "A pronosticar" game (existing rounds, now Nivel 5). // ─────────────────────────────────────────────────────────────── // Shared data — the 3 sources, used across levels // ─────────────────────────────────────────────────────────────── const SOURCES = { cams: { id:'cams', icon:'🛰️', short:'CAMS', name:'CAMS · Satélites', full:'Servicio Copernicus de Vigilancia Atmosférica', where:'El cielo · 36.000 km', color:'#A78BFA', tagline:'Ve todo el planeta', sees:[ 'Polvo del Sahara cruzando el Atlántico', 'Humo de incendios en el Amazonas', 'Cenizas volcánicas, gases industriales', 'Tendencias globales del aire', ], cannot:[ 'Detalle dentro del valle (40 km de píxel)', 'Diferenciar barrios o calles', 'Medir el aire que respiras tú', ], freq:'Cada 12 h · cobertura global', }, wrf: { id:'wrf', icon:'🌬️', short:'WRF', name:'WRF · Modelo meteorológico', full:'Weather Research and Forecasting Model', where:'El valle · simulación 1 km', color:'#0DCCC0', tagline:'Simula el viento del valle', sees:[ 'Viento entre las montañas a cada hora', 'Inversión térmica de madrugada', 'Lluvia, humedad, temperatura', 'Hacia dónde se va a mover el aire', ], cannot:[ 'PM2.5 directamente (no lo mide — lo estima de forma indirecta y con menos precisión)', 'Quemas o emisiones inesperadas', 'Eventos repentinos entre corrida y corrida', ], freq:'1 corrida/día · más de 72 h adelante', }, sens: { id:'sens', icon:'🌡️', short:'Sensores', name:'Sensores en tierra', full:'Red SIATA · 20 estaciones del Valle', where:'Las estaciones · al nivel del suelo', color:'#84BD45', tagline:'Mide la realidad ahora mismo', sees:[ 'PM2.5 real, minuto a minuto', 'Diferencias entre barrios', 'Picos de tráfico o quemas locales', 'Si el modelo se equivocó', ], cannot:[ 'Ver hacia el futuro', 'Cubrir zonas sin estaciones', 'Detectar polvo antes de que llegue', ], freq:'Cada minuto · 20 puntos del Valle', }, }; // ═══════════════════════════════════════════════════════════════ // NIVEL 1 — LAS FUENTES (Sources discovery + mini-quiz) // ═══════════════════════════════════════════════════════════════ function Level1Sources({onNext}){ const [flipped, setFlipped] = React.useState({}); const [quizAns, setQuizAns] = React.useState({}); const QUIZ = [ {q:'¿Quién detecta polvo del Sahara antes de que llegue?', correct:'cams'}, {q:'¿Quién mide el aire que respiras en este momento?', correct:'sens'}, {q:'¿Quién simula hacia dónde va el viento del valle?', correct:'wrf'}, ]; const allFlipped = Object.keys(SOURCES).every(k => flipped[k]); const allAnswered = QUIZ.every((_,i) => quizAns[i]); const allCorrect = QUIZ.every((q,i) => quizAns[i] === q.correct); return (
{Object.values(SOURCES).map((s, i) => ( setFlipped({...flipped, [s.id]:true})} delay={i} /> ))}
{allFlipped && (
Pequeño reto · empareja

¿A quién le toca cada trabajo?

{QUIZ.map((qq, qi) => { const ans = quizAns[qi]; const isCorrect = ans === qq.correct; return (
{qq.q}
{Object.values(SOURCES).map(s => { const sel = ans === s.id; const showResult = !!ans; const isRight = s.id === qq.correct; return ( ); })}
); })}
{allAnswered && (
Siguiente nivel · qué significa cada uno →
)}
)} {!allFlipped && (
↑ Toca las 3 tarjetas para descubrir el reto
)}
); } function SourceFlipCard({source: s, flipped, onFlip, delay}){ return (
{/* FRONT */}
{s.icon}
Fuente · {s.short}
{s.tagline}
Toca para conocer →
{/* BACK */}
{s.icon}
{s.name}
{s.full}
✓ Lo que ve
    {s.sees.map((t,i)=>(
  • {t}
  • ))}
✗ Lo que NO ve
    {s.cannot.map((t,i)=>(
  • {t}
  • ))}
🕒 {s.freq}
); } // ═══════════════════════════════════════════════════════════════ // NIVEL 2 — QUÉ SIGNIFICA CADA UNO (Scenario sorting) // ═══════════════════════════════════════════════════════════════ const SCENARIOS = [ { id:'sahara', emoji:'🏜️', text:'Una nube de polvo del Sahara va a cruzar el Atlántico y llegar a Colombia en 5 días.', best:'cams', why:'Solo los satélites globales ven el polvo viajando sobre el océano. Los modelos locales y los sensores no lo detectan hasta que ya llegó.', }, { id:'inversion', emoji:'🌫️', text:'Mañana a las 6 a.m. va a haber inversión térmica fuerte: el aire frío se queda atrapado abajo.', best:'wrf', why:'El modelo meteorológico simula la atmósfera y predice cuándo y dónde se va a formar la inversión — los satélites no lo ven, y los sensores se enteran cuando ya está pasando.', }, { id:'leña', emoji:'🔥', text:'En el barrio Manrique alguien está quemando leña ahora mismo y el aire local subió.', best:'sens', why:'Solo los sensores en tierra detectan eventos puntuales y locales en tiempo real. Satélites y modelos no tienen esa resolución.', }, { id:'incendio', emoji:'🌳', text:'Hay incendios forestales en el Amazonas brasilero y el humo se mueve hacia el norte.', best:'cams', why:'CAMS detecta plumas de humo a escala continental. WRF puede simular hacia dónde irá, pero CAMS lo ve primero.', }, { id:'lluvia', emoji:'🌧️', text:'Mañana en la tarde va a llover fuerte y eso va a lavar el aire del valle.', best:'wrf', why:'El modelo meteorológico predice la lluvia y dónde caerá. Los sensores la miden cuando ya cae; los satélites la ven pero no la pronostican.', }, { id:'rio', emoji:'📊', text:'Queremos saber si el aire de Bello tiene más PM2.5 que el aire del Poblado ahora mismo.', best:'sens', why:'Comparar barrios es lo que mejor hacen los sensores: cada estación es un punto fijo midiendo en su zona. Los modelos suavizan; los satélites ni siquiera distinguen barrios.', }, ]; function Level2Meaning({onNext}){ const [picks, setPicks] = React.useState({}); const allDone = SCENARIOS.every(s => picks[s.id]); const correctCount = SCENARIOS.filter(s => picks[s.id] === s.best).length; return (
{SCENARIOS.map((sc, i) => ( setPicks({...picks, [sc.id]:sid})} delay={i} /> ))}
{allDone && (
= 4 ? '#84BD45' : '#FFB300', fontSize:24}}> {correctCount} / {SCENARIOS.length} escenarios acertados
)} {allDone ? 'Siguiente · las matemáticas →' : 'Clasifica los 6 escenarios para continuar'}
); } function ScenarioCard({sc, picked, onPick, delay}){ const correct = picked === sc.best; return (
{sc.emoji}
{sc.text}
{Object.values(SOURCES).map(s => { const sel = picked === s.id; const showResult = !!picked; const isRight = s.id === sc.best; return ( ); })}
{picked && (
{correct ? '¡Correcto!' : 'En realidad lo hace ' + SOURCES[sc.best].short + '.'} {sc.why}
)}
); } // ═══════════════════════════════════════════════════════════════ // NIVEL 3 — LAS MATEMÁTICAS (3 interactive equations) // ═══════════════════════════════════════════════════════════════ function Level3Math({onNext}){ return (
Esto es lo que hace el computador del SIATA cada 6 horas
No es magia. Son sumas, promedios ponderados y diferencias. La inteligencia está en cómo se combinan miles de millones de veces sobre todo el valle.
Siguiente · test de comprensión →
); } // Equation card shell function EqCard({n, title, sub, formula, children, footer}){ return (
Ecuación {n}
{title}
{sub}
{formula}
{children} {footer && (
💡 {footer}
)}
); } // ─── Eq 1: spatial average ────────────────────────────────────── function EqAverage(){ const [highCount, setHighCount] = React.useState(2); const total = 20; const lowVal = 18; const highVal = 90; const avg = ((total - highCount) * lowVal + highCount * highVal) / total; return ( PM2.5valle = (sensor1 + sensor2 + … + sensor20) / 20} footer="Pocas mediciones altas no mueven mucho el promedio — por eso necesitamos ver cada estación, no solo el número global." >
Estaciones con valor alto ({'>'}55 μg)
setHighCount(Number(e.target.value))} />
0 {highCount} de 20 10
{/* Dots viz: 20 stations in 10x2 grid */}
{Array.from({length:total}).map((_,i)=>(
))}
Promedio del valle
μg/m³ · aire {pmLabel(avg)}
); } // ─── Eq 2: data assimilation (model + obs weighting) ──────────── function tempColor(t){ if (t <= 16) return '#7DD3FC'; if (t <= 20) return '#0DCCC0'; if (t <= 24) return '#84BD45'; if (t <= 28) return '#FFB300'; return '#FF8A3D'; } function tempLabel(t){ if (t <= 16) return 'frío'; if (t <= 20) return 'fresco'; if (t <= 24) return 'templado'; if (t <= 28) return 'cálido'; return 'caluroso'; } function EqAssimilation(){ const [alpha, setAlpha] = React.useState(0.5); const model = 22; // °C predicho por WRF const obs = 26; // °C medido por sensores const result = model * (1-alpha) + obs * alpha; return ( Temperatura = Modelo · (1 − α) + Medición · α} footer="Cuando los sensores son confiables, subimos α (les damos más peso). Cuando hay pocos sensores o el modelo simula bien la física, bajamos α y confiamos más en el modelo. Lo mismo aplica para viento, humedad y demás variables meteorológicas." >
Modelo dice
{model}°
°C · WRF
Sensores miden
{obs}°
°C · estaciones
Peso de la medición · α = {alpha.toFixed(2)}
setAlpha(Number(e.target.value))} />
0 · creer al modelo 1 · creer al sensor
{/* Mix bar */}
Temperatura final
°
°C · ambiente {tempLabel(result)}
); } // ─── Eq 3: evolution over time ────────────────────────────────── function EqEvolution(){ const [pm, setPm] = React.useState(30); const [emis, setEmis] = React.useState(10); const [wind, setWind] = React.useState(5); // hour-ahead const next = Math.max(2, pm + emis - wind*2); return ( PM2.5próxima hora = PM2.5ahora + Emisión − Dispersión} footer="Por eso el viento es tu aliado: cada km/h de viento puede llevarse 2 μg/m³ por hora. Por eso los días de calma + inversión térmica son los peores." >
Ahora
{pm}
En 1 hora
pm ? '#FF8A3D' : '#84BD45', fontWeight:600, }}> {next > pm ? '↑ Empeorando' : next < pm ? '↓ Mejorando' : '= Sin cambio'} {' · '} cambia {Math.abs(next-pm).toFixed(0)} μg/m³
); } function EvolSlider({label, value, setValue, min, max, step, unit, color}){ return (
{label}
{value} {unit}
setValue(Number(e.target.value))} />
); } // ═══════════════════════════════════════════════════════════════ // NIVEL 4 — TEST DE COMPRENSIÓN (5-question quiz) // ═══════════════════════════════════════════════════════════════ const QUIZ_Q = [ { q:'Si llega una nube de polvo del Sahara hacia Colombia, ¿qué fuente te avisa primero?', opts:[ {id:'a', t:'Un sensor en el centro de Medellín'}, {id:'b', t:'CAMS · los satélites globales'}, {id:'c', t:'El modelo meteorológico WRF'}, ], correct:'b', why:'Los satélites de CAMS son los únicos que ven el aire del planeta entero. Detectan el polvo cuando aún está cruzando el Atlántico, días antes de que llegue.', }, { q:'Una mañana hay inversión térmica fuerte sobre el valle. ¿Qué le pasa al PM2.5?', opts:[ {id:'a', t:'Se queda atrapado y sube'}, {id:'b', t:'Sube al cielo y se dispersa'}, {id:'c', t:'No le pasa nada'}, ], correct:'a', why:'La inversión térmica es como una "tapa" de aire caliente encima del aire frío. El material particulado no puede subir y se queda atrapado entre las montañas — por eso las mañanas son la peor hora.', }, { q:'¿Por qué necesitamos sensores en tierra si ya tenemos satélites y modelos?', opts:[ {id:'a', t:'Para tener más logos en la página'}, {id:'b', t:'Porque sensores, modelos y satélites compiten'}, {id:'c', t:'Porque sólo los sensores miden el aire que respiramos de verdad'}, ], correct:'c', why:'Satélites y modelos hacen estimaciones. Los sensores son la "verdad" sobre el suelo: corrigen los modelos cuando se equivocan y miden lo que de verdad estás respirando ahora mismo.', }, { q:'El pronóstico dice 50 μg/m³ ± 8 μg/m³. ¿Qué significa el "± 8"?', opts:[ {id:'a', t:'Que el aire va a oscilar entre 42 y 58 a lo largo del día'}, {id:'b', t:'La incertidumbre: probablemente esté entre 42 y 58 μg/m³'}, {id:'c', t:'Que hay 8 sensores funcionando'}, ], correct:'b', why:'Ningún pronóstico es exacto. El "±" indica el margen de error: el SIATA está bastante seguro de que el valor real estará dentro de ese rango. Mientras más datos, menor el ±.', }, { q:'Es viernes a las 6 a.m. y el pronóstico de hoy dice PM2.5 alto en la mañana, bajo en la tarde. ¿Cuál es la mejor decisión para hacer ejercicio?', opts:[ {id:'a', t:'Salir a trotar ya mismo, antes de que empeore'}, {id:'b', t:'Esperar a la tarde, cuando baje'}, {id:'c', t:'No ejercitarse hoy'}, ], correct:'b', why:'El pronóstico te da exactamente esto: saber CUÁNDO el aire estará mejor. Esperar unas horas convierte un día "malo" en una ventana saludable — esa es la idea de pronosticar.', }, ]; function Level4Quiz({onNext, store, setStore}){ const [answers, setAnswers] = React.useState(store.quiz || {}); const [shown, setShown] = React.useState({}); const pick = (qi, aid) => { if(answers[qi]) return; const next = {...answers, [qi]: aid}; setAnswers(next); setStore({...store, quiz: next}); setTimeout(()=> setShown({...shown, [qi]: true}), 200); }; const allDone = QUIZ_Q.every((_,i) => answers[i]); const score = QUIZ_Q.filter((q,i) => answers[i] === q.correct).length; return (
{QUIZ_Q.map((q, i) => ( pick(i, aid)} /> ))}
{allDone && (
= 4 ? 'linear-gradient(135deg, rgba(132,189,69,0.15), rgba(13,204,192,0.08))' : 'linear-gradient(135deg, rgba(255,179,0,0.12), rgba(232,115,90,0.05))', border:'1px solid ' + (score >= 4 ? 'rgba(132,189,69,0.5)' : 'rgba(255,179,0,0.5)'), }}>
=4?'#84BD45':'#FFB300', letterSpacing:'0.24em', textTransform:'uppercase', }}> {score >= 4 ? 'Aprobaste' : 'Casi'}
=4?'#84BD45':'#FFB300', letterSpacing:'-0.03em', }}>{score} / 5
{score === 5 && '¡Perfecto! Tienes una comprensión completa del sistema.'} {score === 4 && 'Muy bien. Entiendes lo esencial — listo para el último nivel.'} {score === 3 && 'Bien. Revisa los niveles anteriores si quieres afinar, pero puedes seguir.'} {score <= 2 && 'Vale la pena volver a los niveles anteriores. El último nivel funciona mejor si tienes claros los conceptos.'}
Último nivel · ¡a pronosticar! →
)}
); } function QuizQuestion({num, q, answer, onPick}){ const showResult = !!answer; const isCorrect = answer === q.correct; return (
0{num}
{q.q}
{q.opts.map(o => { const sel = answer === o.id; const isRight = o.id === q.correct; return ( ); })}
{showResult && (
{isCorrect ? '✓ Correcto.' : 'Era ' + q.opts.find(o=>o.id===q.correct).t + '.'} {q.why}
)}
); } // ─────────────────────────────────────────────────────────────── // Shared LevelHead // ─────────────────────────────────────────────────────────────── function LevelHead({n, total, name, title, sub}){ return (
Nivel {n} / {total} {name}

{title}

{sub && (

{sub}

)}
); } Object.assign(window, { Level1Sources, Level2Meaning, Level3Math, Level4Quiz, LevelHead, SOURCES, });