// 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 (
);
}
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 (
);
}
// ─── 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 2010
{/* 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 modelo1 · 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."
>
);
}
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 (
{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 (