// 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
{!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 });