// Activities part 1: Memory, Match, Order, Fill, Answer // CreateJS used in Puzzle and Memory. const { useState: useStateA, useEffect: useEffectA, useRef: useRefA, useMemo: useMemoA, useCallback: useCallbackA } = React; // Helpers function shuffle(arr) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } function pickN(arr, n) { return shuffle(arr).slice(0, n); } function range(n) { return Array.from({length:n}, (_,i)=>i); } // ========================================================= // 1. MEMORIA — pairs of cards (operation ↔ result) // ========================================================= function ActMemory({ theme, difficulty, onCorrect, onMistake, onFinish, data }) { const pairsCount = { facil: 4, media: 6, dificil: 8 }[difficulty] || 6; const cards = useMemoA(() => { if (Array.isArray(data?.pares) && data.pares.length) { const rawPairs = pickN(data.pares, Math.min(pairsCount, data.pares.length)); const deck = []; rawPairs.forEach((p, i) => { const left = String(p?.concepto ?? '').trim(); const right = String(p?.respuesta ?? '').trim(); if (!left || !right) return; deck.push({ id: `e${i}`, pair: i, label: left }); deck.push({ id: `r${i}`, pair: i, label: right }); }); if (deck.length >= 4) return shuffle(deck); } const ops = []; while (ops.length < pairsCount) { const a = 2 + Math.floor(Math.random()*9); const b = 2 + Math.floor(Math.random()*9); const op = Math.random() < 0.5 ? "+" : "×"; const r = op === "+" ? a + b : a * b; const expr = `${a}${op}${b}`; if (!ops.some(o => o.r === r)) ops.push({ expr, r }); } const deck = []; ops.forEach((o, i) => { deck.push({ id: `e${i}`, pair: i, label: o.expr }); deck.push({ id: `r${i}`, pair: i, label: String(o.r) }); }); return shuffle(deck); }, [pairsCount, data]); const [flipped, setFlipped] = useStateA([]); const [matched, setMatched] = useStateA(new Set()); const [mistakes, setMistakes] = useStateA(0); const lock = useRefA(false); useEffectA(() => { if (matched.size === cards.length && cards.length > 0) { const correct = cards.length / 2 - mistakes; setTimeout(() => onFinish(Math.max(1, correct), cards.length / 2), 800); } }, [matched, cards.length, mistakes, onFinish]); const click = (i) => { if (lock.current || flipped.includes(i) || matched.has(cards[i].id)) return; window.SFX && window.SFX.pickup(); const next = [...flipped, i]; setFlipped(next); if (next.length === 2) { lock.current = true; const [a, b] = next; if (cards[a].pair === cards[b].pair) { setTimeout(() => { setMatched(m => new Set([...m, cards[a].id, cards[b].id])); setFlipped([]); lock.current = false; onCorrect(); }, 500); } else { setMistakes(m => m + 1); setTimeout(() => { setFlipped([]); lock.current = false; onMistake(); }, 900); } } }; const cols = pairsCount === 4 ? 4 : pairsCount === 6 ? 4 : 4; return (
Empareja cada operación con su resultado.
{cards.map((c, i) => { const show = flipped.includes(i) || matched.has(c.id); const isMatched = matched.has(c.id); const labelText = String(c.label || ''); const labelLen = labelText.length; const fontSizeStyle = labelLen > 24 ? 'calc(11px * var(--font-scale))' : labelLen > 16 ? 'calc(14px * var(--font-scale))' : labelLen > 10 ? 'calc(18px * var(--font-scale))' : labelLen > 6 ? 'calc(22px * var(--font-scale))' : undefined; return ( ); })}
); } // ========================================================= // 2. ASOCIACIÓN — match operations to results (drag lines) // ========================================================= function ActMatch({ theme, difficulty, onCorrect, onMistake, onFinish, data }) { const count = { facil: 4, media: 5, dificil: 6 }[difficulty] || 5; const pairs = useMemoA(() => { if (Array.isArray(data?.pares) && data.pares.length) { return pickN(data.pares, Math.min(count, data.pares.length)).map((p, idx) => ({ expr: String(p?.concepto ?? ''), r: String(p?.respuesta ?? ''), id: idx, })).filter((p) => p.expr && p.r); } const out = []; while (out.length < count) { const a = 2 + Math.floor(Math.random()*8); const b = 2 + Math.floor(Math.random()*8); const op = ["+","-","×"][Math.floor(Math.random()*3)]; let r; if (op === "+") r = a + b; else if (op === "-") r = Math.max(a,b) - Math.min(a,b); else r = a * b; const expr = op === "-" ? `${Math.max(a,b)}-${Math.min(a,b)}` : `${a}${op}${b}`; if (!out.some(o => o.r === r)) out.push({ expr, r, id: out.length }); } return out; }, [count, data]); const [rights] = useStateA(() => shuffle(pairs.map(p => p.r))); const [matches, setMatches] = useStateA({}); // {leftId: rightValue} const [selected, setSelected] = useStateA(null); const [mistakes, setMistakes] = useStateA(0); useEffectA(() => { if (Object.keys(matches).length === pairs.length && pairs.every(p => matches[p.id] === p.r)) { setTimeout(() => onFinish(pairs.length - mistakes, pairs.length), 600); } }, [matches, pairs.length, mistakes, onFinish]); const pickLeft = (id) => { if (matches[id] != null) return; setSelected({ side: "L", id }); window.SFX && window.SFX.pickup(); }; const pickRight = (val) => { if (Object.values(matches).includes(val)) return; if (!selected || selected.side !== "L") { setSelected({ side: "R", val }); return; } const left = pairs.find(p => p.id === selected.id); if (left.r === val) { setMatches({ ...matches, [selected.id]: val }); setSelected(null); onCorrect(); } else { setMistakes(m => m + 1); setSelected(null); onMistake(); } }; return (
Toca una operación y luego su resultado.
{pairs.map(p => { const done = matches[p.id] != null; const sel = selected?.side === "L" && selected.id === p.id; return ( ); })}
{rights.map((r, i) => { const done = Object.values(matches).includes(r); const sel = selected?.side === "R" && selected.val === r; return ( ); })}
); } // ========================================================= // 3. ORDENAR — drag-arrange numbers in order // ========================================================= function ActOrder({ theme, difficulty, onCorrect, onMistake, onFinish, data }) { const count = { facil: 5, media: 6, dificil: 8 }[difficulty] || 6; const direction = useMemoA(() => { if (data?.orden === 'asc' || data?.orden === 'desc') return data.orden; return Math.random() < 0.5 ? "asc" : "desc"; }, [data]); const correct = useMemoA(() => { if (Array.isArray(data?.items) && data.items.length >= 3) { const arr = data.items.map((n) => Number(n)).filter((n) => Number.isFinite(n)); if (arr.length >= 3) { arr.sort((a, b) => direction === "asc" ? a - b : b - a); return arr; } } const set = new Set(); while (set.size < count) set.add(Math.floor(Math.random()*90)+10); const arr = [...set]; arr.sort((a,b) => direction === "asc" ? a-b : b-a); return arr; }, [count, direction, data]); const [items, setItems] = useStateA(() => shuffle([...correct])); const [checked, setChecked] = useStateA(false); const [picking, setPicking] = useStateA(null); const swap = (i, j) => { const next = [...items]; [next[i], next[j]] = [next[j], next[i]]; setItems(next); window.SFX && window.SFX.pickup(); }; const onTap = (i) => { if (picking == null) { setPicking(i); window.SFX && window.SFX.pickup(); } else if (picking === i) { setPicking(null); } else { swap(picking, i); setPicking(null); } }; const check = () => { setChecked(true); const right = items.filter((v,i)=>v===correct[i]).length; if (right === items.length) { onCorrect(); setTimeout(() => onFinish(items.length, items.length), 500); } else { onMistake(); setTimeout(() => setChecked(false), 1200); } }; return (
Ordena los números de {direction === "asc" ? "menor a mayor" : "mayor a menor"}.
{items.map((n, i) => { const isRight = checked && n === correct[i]; const isWrong = checked && n !== correct[i]; return ( ); })}
{direction === "asc" ? "menor → → mayor" : "mayor → → menor"}
); } // ========================================================= // 4. COMPLETAR — fill the missing operand // ========================================================= function ActFill({ theme, difficulty, onCorrect, onMistake, onFinish, data }) { const count = { facil: 4, media: 5, dificil: 7 }[difficulty] || 5; const probs = useMemoA(() => { if (typeof data?.texto === 'string' && Array.isArray(data?.respuestas) && data.respuestas.length) { return [{ mode: 'custom', text: data.texto, ans: data.respuestas.map(r => String(r).trim()), }]; } const out = []; for (let i = 0; i < count; i++) { const a = 2 + Math.floor(Math.random()*9); const b = 2 + Math.floor(Math.random()*9); const ops = ["+", "-", "×"]; const op = ops[Math.floor(Math.random()*ops.length)]; let r; if (op === "+") r = a + b; else if (op === "-") r = Math.max(a,b) - Math.min(a,b); else r = a * b; const a2 = op === "-" ? Math.max(a,b) : a; const b2 = op === "-" ? Math.min(a,b) : b; // hide one of: a, b, or r const hide = ["a","b","r"][Math.floor(Math.random()*3)]; out.push({ a: a2, b: b2, op, r, hide, ans: hide==="a"?a2:hide==="b"?b2:r }); } return out; }, [count, data]); const [vals, setVals] = useStateA(() => probs.map(p => p.mode === 'custom' ? p.ans.map(()=>"") : "")); const [checked, setChecked] = useStateA([]); const check = () => { const next = vals.map((v, i) => { if (probs[i].mode === 'custom') { const allCorrect = probs[i].ans.every((ans, j) => String(v[j]||'').trim().toLowerCase() === String(ans).toLowerCase()); return allCorrect ? 'ok' : 'no'; } return parseInt(v, 10) === probs[i].ans ? 'ok' : 'no'; }); setChecked(next); const right = next.filter(x=>x==="ok").length; if (right === probs.length) { onCorrect(); setTimeout(() => onFinish(right, probs.length), 600); } else { const wrong = probs.length - right; for (let i=0;i onFinish(right, probs.length), 1200); } }; const isCustom = probs.some(p => p.mode === 'custom'); const instruction = isCustom ? "Completa el texto con las palabras correctas." : "Escribe el número que falta en cada operación."; return (
{instruction}
{probs.map((p, i) => (
{i+1}. {p.mode === 'custom' ? (() => { const parts = p.text.split(/_+/); return ( {parts.map((part, partIdx) => ( {part} {partIdx < parts.length - 1 && partIdx < p.ans.length && ( { const n = [...vals]; const arr = [...n[i]]; arr[partIdx] = v; n[i] = arr; setVals(n); }} state={checked[i]} /> )} ))} ); })() : ( {p.hide === "a" ? {const n=[...vals];n[i]=v;setVals(n);}} state={checked[i]}/> : {p.a}} {p.op} {p.hide === "b" ? {const n=[...vals];n[i]=v;setVals(n);}} state={checked[i]}/> : {p.b}} = {p.hide === "r" ? {const n=[...vals];n[i]=v;setVals(n);}} state={checked[i]}/> : {p.r}} )}
))}
); } function FillBox({ value, onChange, state, type = "number" }) { return ( onChange(e.target.value)} /> ); } // ========================================================= // 8. RESPUESTA ESCRITA — answer math problems // ========================================================= function ActAnswer({ theme, difficulty, onCorrect, onMistake, onFinish, attemptsLeft, data }) { const count = { facil: 5, media: 7, dificil: 10 }[difficulty] || 7; const isCustom = typeof data?.pregunta === 'string' || (Array.isArray(data?.preguntas) && data.preguntas.length > 0); const probs = useMemoA(() => { if (Array.isArray(data?.preguntas) && data.preguntas.length > 0) { return data.preguntas.map(p => ({ expr: String(p.pregunta || p.q || ''), r: String(p.respuesta || p.respuesta_correcta || p.a || ''), })).filter(p => p.expr && p.r); } if (typeof data?.pregunta === 'string' && data?.respuesta_correcta != null) { return [{ expr: data.pregunta, r: String(data.respuesta_correcta) }]; } const out = []; for (let i = 0; i < count; i++) { const a = 5 + Math.floor(Math.random()*15); const b = 2 + Math.floor(Math.random()*10); const op = ["+", "-", "×", "÷"][Math.floor(Math.random()*4)]; let r, expr; if (op === "+") { r = a + b; expr = `${a} + ${b}`; } else if (op === "-") { r = a - b; expr = `${a} - ${b}`; } else if (op === "×") { r = a * b; expr = `${a} × ${b}`; } else { r = a; expr = `${a*b} ÷ ${b}`; } out.push({ expr, r }); } return out; }, [count, data]); const [idx, setIdx] = useStateA(0); const [val, setVal] = useStateA(""); const [right, setRight] = useStateA(0); const [feedback, setFeedback] = useStateA(null); const inputRef = useRefA(); useEffectA(() => { inputRef.current?.focus(); }, [idx]); const submit = (e) => { e?.preventDefault(); if (val === "") return; const expected = probs[idx].r; const ok = String(val).trim().toLowerCase() === String(expected).trim().toLowerCase(); setFeedback(ok ? "ok" : "no"); if (ok) { onCorrect(); setRight(r => r + 1); } else onMistake(); setTimeout(() => { setFeedback(null); setVal(""); if (idx + 1 < probs.length) setIdx(i => i + 1); else onFinish(right + (ok?1:0), probs.length); }, 700); }; const p = probs[idx]; const instruction = isCustom ? "Lee la pregunta y escribe tu respuesta." : "Resuelve cada operación y escribe el resultado."; return (
{instruction}
Pregunta {idx+1} de {probs.length} · {right} aciertos
{p.expr}{isCustom ? "" : " ="}
setVal(e.target.value)} placeholder="Escribe aquí..."/>
{!isCustom && ( { if (d === "←") setVal(v => String(v).slice(0,-1)); else if (d === "OK") submit(); else setVal(v => (String(v) + d).slice(0,4)); }}/> )}
); } function Numpad({ onPress }) { const keys = ["1","2","3","4","5","6","7","8","9","←","0","OK"]; return (
{keys.map(k => ( ))}
); } Object.assign(window, { ActMemory, ActMatch, ActOrder, ActFill, ActAnswer, shuffle, pickN, range });