// Activities part 2: WordSearch, Crossword, Puzzle (CreateJS) const { useState: useStateB, useEffect: useEffectB, useRef: useRefB, useMemo: useMemoB, useCallback: useCallbackB } = React; // ========================================================= // 5. SOPA DE NÚMEROS — find numeric results in a grid // ========================================================= function ActWordSearch({ theme, difficulty, onCorrect, onMistake, onFinish, data }) { const cfg = { facil: { size: 8, count: 4 }, media: { size: 10, count: 5 }, dificil: { size: 12, count: 6 } }[difficulty] || { size: 10, count: 5 }; const puzzle = useMemoB(() => { let targets = []; let isWords = false; let customTitle = "Encuentra las palabras en la sopa de letras."; if (Array.isArray(data?.palabras) && data.palabras.length > 0) { isWords = true; targets = data.palabras .map(w => typeof w === 'string' ? w : (w.palabra || w.word || w.concepto || '')) .map(w => String(w).toUpperCase().replace(/[^A-ZÁÉÍÓÚÑ]/g, '')) .filter(w => w.length > 1) .map(w => ({ expr: w, r: w, str: w })); } else if (Array.isArray(data?.conceptos) && data.conceptos.length > 0) { // Fallback for some AI formats isWords = true; targets = data.conceptos .map(w => String(w).toUpperCase().replace(/[^A-ZÁÉÍÓÚÑ]/g, '')) .filter(w => w.length > 1) .map(w => ({ expr: w, r: w, str: w })); } if (!isWords || targets.length === 0) { isWords = false; customTitle = "Encuentra los resultados de cada operación en la sopa."; while (targets.length < cfg.count) { 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 str = String(r); if (str.length >= 2 && str.length <= 3 && !targets.some(t => t.r === r)) { targets.push({ expr: `${a}${op}${b}`, r, str }); } } } const maxLen = Math.max(...targets.map(t => t.str.length), 0); const N = Math.max(cfg.size, maxLen + 2, 8); const grid = Array.from({length: N}, () => Array.from({length: N}, () => null)); const placements = []; targets.sort((a, b) => b.str.length - a.str.length); const dirs = [[1,0],[0,1],[1,1],[-1,1],[1,-1],[0,-1],[-1,0]]; // more directions targets.forEach(t => { let placed = false; for (let tries = 0; tries < 200 && !placed; tries++) { const dir = dirs[Math.floor(Math.random()*dirs.length)]; const len = t.str.length; const r0 = Math.floor(Math.random()*N); const c0 = Math.floor(Math.random()*N); const cells = []; let ok = true; for (let i = 0; i < len; i++) { const r = r0 + dir[1]*i, c = c0 + dir[0]*i; if (r<0||r>=N||c<0||c>=N) { ok=false; break; } if (grid[r][c] != null && grid[r][c] !== t.str[i]) { ok=false; break; } cells.push([r,c]); } if (ok) { cells.forEach(([r,c],i) => { grid[r][c] = t.str[i]; }); placements.push({ ...t, cells }); placed = true; } } }); const alphabet = "ABCDEFGHIJKLMNÑOPQRSTUVWXYZ"; for (let r=0;r { if (found.length === puzzle.placements.length) { setTimeout(() => onFinish(found.length, puzzle.placements.length), 600); } }, [found.length]); const cellKey = (r,c) => `${r}-${c}`; const isInPath = (r,c) => path.some(([pr,pc]) => pr===r && pc===c); const isFound = (r,c) => found.some(f => f.cells.some(([pr,pc]) => pr===r && pc===c)); const startPath = (r,c) => { setPath([[r,c]]); setDrag(true); window.SFX && window.SFX.pickup(); }; const extendPath = (r,c) => { if (!drag) return; if (path.length === 0) return; if (isInPath(r,c)) return; // Constrain to straight line from start const [sr, sc] = path[0]; const dr = r - sr, dc = c - sc; if (!(dr === 0 || dc === 0 || Math.abs(dr) === Math.abs(dc))) return; const len = Math.max(Math.abs(dr), Math.abs(dc)); const stepR = len === 0 ? 0 : dr / len, stepC = len === 0 ? 0 : dc / len; const cells = []; for (let i = 0; i <= len; i++) cells.push([sr + stepR*i, sc + stepC*i]); setPath(cells); }; const endPath = () => { setDrag(false); if (path.length < 2) { setPath([]); return; } const str = path.map(([r,c]) => puzzle.grid[r][c]).join(""); const strRev = path.map(([r,c]) => puzzle.grid[r][c]).reverse().join(""); const match = puzzle.placements.find(p => !found.some(f => f.r === p.r) && (p.str === str || p.str === strRev) ); if (match) { setFound(f => [...f, { cells: path, r: match.r, expr: match.expr }]); onCorrect(); } else { onMistake(); } setPath([]); }; return (
{puzzle.title}
{puzzle.grid.map((row, r) => row.map((ch, c) => { const inP = isInPath(r,c), fnd = isFound(r,c); return (
startPath(r,c)} onMouseEnter={()=>extendPath(r,c)} onTouchStart={()=>startPath(r,c)} onTouchMove={(e)=>{ const t = e.touches[0]; const el = document.elementFromPoint(t.clientX, t.clientY); if (el?.dataset?.ws) { const [rr,cc] = el.dataset.ws.split("-").map(Number); extendPath(rr,cc); } }} data-ws={cellKey(r,c)}> {ch}
); }))}
Encuentra:
{puzzle.placements.map(p => { const done = found.some(f => f.r === p.r); return (
{p.expr} = {done ? p.r : "?"}
); })}
); } // ========================================================= // 6. CRUCIGRAMA NUMÉRICO — fill numbers across/down // ========================================================= function ActCrossword({ theme, difficulty, onCorrect, onMistake, onFinish, data }) { const layout = useMemoB(() => { let customPistas = []; if (Array.isArray(data?.pistas) && data.pistas.length) { customPistas = data.pistas .map((p) => ({ pista: String(p?.pista ?? ''), respuesta: String(p?.respuesta ?? '').replace(/\s+/g, '').toUpperCase(), })) .filter((p) => p.pista && p.respuesta); } if (customPistas.length > 0) { const MAX_N = 16; const gridChars = Array.from({length: MAX_N}, () => Array.from({length: MAX_N}, () => null)); const slots = []; const answers = {}; const clues = {}; let idCounter = 1; // Sort by length to place longest words first customPistas.sort((a,b) => b.respuesta.length - a.respuesta.length); customPistas.forEach((item, idx) => { const word = item.respuesta; let placed = false; if (idx === 0) { const r = Math.floor(MAX_N/2); const c = Math.floor((MAX_N - word.length)/2); for (let i=0; i MAX_N) canH = false; else { for (let i=0; i0 && gridChars[r-1][hc+i] != null) || (r0 && gridChars[r][hc-1] != null) canH = false; if (canH && hc+word.length MAX_N) canV = false; else { for (let i=0; i0 && gridChars[vr+i][c-1] != null) || (c0 && gridChars[vr-1][c] != null) canV = false; if (canV && vr+word.length MAX_N) can = false; else { for (let i=0; i0 && gridChars[r-1][c+i] != null) || (r0 && gridChars[r][c-1] != null) can = false; if (can && c+word.length MAX_N) can = false; else { for (let i=0; i0 && gridChars[r+i][c-1] != null) || (c0 && gridChars[r-1][c] != null) can = false; if (can && r+word.length { if (s.r < minR) minR = s.r; if (s.c < minC) minC = s.c; if (s.dir === "H") { if (s.r > maxR) maxR = s.r; if (s.c + s.len - 1 > maxC) maxC = s.c + s.len - 1; } else { if (s.r + s.len - 1 > maxR) maxR = s.r + s.len - 1; if (s.c > maxC) maxC = s.c; } }); if (minR > maxR) { minR = 0; maxR = 0; minC = 0; maxC = 0; } const pad = 1; minR = Math.max(0, minR - pad); minC = Math.max(0, minC - pad); maxR = Math.min(MAX_N - 1, maxR + pad); maxC = Math.min(MAX_N - 1, maxC + pad); const finalN = Math.max(maxR - minR + 1, maxC - minC + 1, 5); const active = Array.from({length: finalN}, () => Array.from({length: finalN}, () => false)); slots.forEach(s => { s.r -= minR; s.c -= minC; for (let i=0; i answers[s.id] = String(genNumber(s.len))); let ok = true; slots.forEach(s => { for (let i = 0; i < s.len; i++) { const r = s.dir==="H" ? s.r : s.r+i; const c = s.dir==="H" ? s.c+i : s.c; slots.forEach(o => { if (o.id === s.id) return; for (let j = 0; j < o.len; j++) { const or = o.dir==="H" ? o.r : o.r+j; const oc = o.dir==="H" ? o.c+j : o.c; if (or === r && oc === c) { if (answers[s.id][i] !== answers[o.id][j]) ok = false; } } }); } }); if (ok) break; } if (Object.keys(answers).length < slots.length) { answers = { "1H": "123", "2H": "4567", "3H": "890", "1V": "32850", "2V": "5670" }; } function makeClue(num) { const n = parseInt(num, 10); const f = 2 + Math.floor(Math.random()*8); if (n % f === 0) return `${n/f} × ${f}`; const s = Math.max(1, Math.floor(n/2)); return `${s} + ${n - s}`; } const clues = {}; slots.forEach(s => clues[s.id] = makeClue(answers[s.id])); const active = Array.from({length:N}, () => Array.from({length:N}, () => false)); slots.forEach(s => { for (let i = 0; i < s.len; i++) { const r = s.dir==="H" ? s.r : s.r+i; const c = s.dir==="H" ? s.c+i : s.c; active[r][c] = true; } }); return { N, slots, answers, clues, active, title: "Resuelve cada operación y escribe el resultado en el crucigrama." }; }, [difficulty, data]); const [grid, setGrid] = useStateB(() => Array.from({length: layout.N}, () => Array.from({length: layout.N}, () => "")) ); const [checked, setChecked] = useStateB(false); const set = (r, c, v) => { if (v.length > 1) v = v.slice(-1); if (v && !/[A-Za-z0-9ÁÉÍÓÚáéíóúÑñ]/.test(v)) return; const next = grid.map(row => [...row]); next[r][c] = String(v).toUpperCase(); setGrid(next); }; const check = () => { setChecked(true); let allRight = true; layout.slots.forEach(s => { const got = []; for (let i = 0; i < s.len; i++) { const r = s.dir==="H" ? s.r : s.r+i; const c = s.dir==="H" ? s.c+i : s.c; got.push(grid[r][c]); } if (got.join("") !== layout.answers[s.id]) allRight = false; }); if (allRight) { onCorrect(); setTimeout(() => onFinish(layout.slots.length, layout.slots.length), 500); } else { onMistake(); setTimeout(() => setChecked(false), 1200); } }; const labels = {}; let n = 1; layout.slots.forEach(s => { const k = `${s.r}-${s.c}`; if (!labels[k]) { labels[k] = n++; } }); return (
{layout.title}
{Array.from({length: layout.N}).map((_, r) => Array.from({length: layout.N}).map((_, c) => { if (!layout.active[r][c]) return
; const isStart = layout.slots.some(s => s.r===r && s.c===c); const correct = checked && grid[r][c] === (() => { const sl = layout.slots.find(s => (s.dir==="H" && s.r===r && s.c<=c && s.c+s.len>c) || (s.dir==="V" && s.c===c && s.r<=r && s.r+s.len>r)); if (sl) return layout.answers[sl.id][sl.dir==="H" ? c - sl.c : r - sl.r]; return null; })(); return (
{isStart && {labels[`${r}-${c}`]}} set(r,c,e.target.value)}/>
); }))}
Horizontales
{layout.slots.filter(s=>s.dir==="H").map(s => (
{labels[`${s.r}-${s.c}`]}. {layout.clues[s.id]}
))}
Verticales
{layout.slots.filter(s=>s.dir==="V").map(s => (
{labels[`${s.r}-${s.c}`]}. {layout.clues[s.id]}
))}
); } // ========================================================= // 7. ROMPECABEZAS (CreateJS) — slide puzzle // ========================================================= function ActPuzzle({ theme, difficulty, onCorrect, onMistake, onFinish, data }) { const N = { facil: 3, media: 3, dificil: 4 }[difficulty] || 3; const canvasRef = useRefB(null); const stageRef = useRefB(null); const [solved, setSolved] = useStateB(false); const tilesRef = useRefB([]); const orderRef = useRefB([]); const SIZE = 360; const T = SIZE / N; useEffectB(() => { if (!window.createjs || !canvasRef.current) return; const stage = new createjs.Stage(canvasRef.current); stage.enableMouseOver(20); stageRef.current = stage; const colors = [theme.primary, theme.secondary, theme.accent, theme.success, theme.star]; const tiles = []; for (let i = 0; i < N*N; i++) { const tile = new createjs.Container(); const r = Math.floor(i/N), c = i%N; const bg = new createjs.Shape(); const col = colors[(r*7+c*3)%colors.length]; bg.graphics.beginFill(col).drawRoundRect(2,2,T-4,T-4,8); const txtStr = (i === N*N - 1) ? "" : String(i + 1); const txt = new createjs.Text(txtStr, `bold ${T*0.5}px sans-serif`, "#ffffff"); txt.textAlign = "center"; txt.textBaseline = "middle"; txt.x = T/2; txt.y = T/2; const border = new createjs.Shape(); border.graphics.setStrokeStyle(3).beginStroke("rgba(0,0,0,0.25)").drawRoundRect(2,2,T-4,T-4,8); tile.addChild(bg, txt, border); tile.regX = 0; tile.regY = 0; tile.targetIndex = i; tile.on("click", () => tryMove(tile)); tiles.push(tile); } tiles[N*N-1].alpha = 0; tiles[N*N-1].mouseEnabled = false; const order = tiles.slice(); // shuffle by valid moves to ensure solvability for (let s = 0; s < 100; s++) { const empty = order.findIndex(t => t.alpha === 0); const er = Math.floor(empty/N), ec = empty%N; const cands = []; [[-1,0],[1,0],[0,-1],[0,1]].forEach(([dr,dc]) => { const nr = er+dr, nc = ec+dc; if (nr>=0 && nr=0 && nc { const r = Math.floor(idx/N), c = idx%N; createjs.Tween.get(t, { override: true }).to({ x: c*T, y: r*T }, 180, createjs.Ease.quadOut); }); stage.addChild(...orderRef.current); stage.update(); } function checkWin() { const ord = orderRef.current; const won = ord.every((t, i) => t.targetIndex === i); if (won) { setSolved(true); ord[N*N-1].alpha = 1; ord[N*N-1].mouseEnabled = false; stage.update(); onCorrect(); setTimeout(() => onFinish(N*N, N*N), 800); } } function tryMove(tile) { if (solved) return; const ord = orderRef.current; const idx = ord.indexOf(tile); const empty = ord.findIndex(t => t.alpha === 0); const ir = Math.floor(idx/N), ic = idx%N; const er = Math.floor(empty/N), ec = empty%N; if (Math.abs(ir-er)+Math.abs(ic-ec) === 1) { [ord[idx], ord[empty]] = [ord[empty], ord[idx]]; layout(); window.SFX && window.SFX.pickup(); checkWin(); } } function giveHint() { if (solved) return; const ord = orderRef.current; for (let i = 0; i < N*N - 1; i++) { if (ord[i].targetIndex !== i) { const correctIdx = ord.findIndex(t => t.targetIndex === i); [ord[i], ord[correctIdx]] = [ord[correctIdx], ord[i]]; layout(); window.SFX && window.SFX.pickup(); checkWin(); break; } } } tilesRef.current = { tiles, layout, tryMove, giveHint }; layout(); createjs.Ticker.framerate = 30; createjs.Ticker.addEventListener("tick", stage); return () => { createjs.Ticker.removeEventListener("tick", stage); stage.removeAllChildren(); }; }, [N, theme, solved]); // added solved so state syncs if needed return (
Ordena los números del 1 al {N*N - 1} deslizando las piezas.
Toca una pieza junto al hueco para moverla.
{!solved && ( )}
); } Object.assign(window, { ActWordSearch, ActCrossword, ActPuzzle });