/** * minigames/memory.js * 🃏 Memory — Finde alle Paare, so schnell wie möglich! */ window.MG_memory = (function() { const ID = 'memory'; const EMOJI = '🃏'; const NAME = 'Memory'; const DESC = 'Finde alle Paare — so schnell wie möglich!'; const CONTROLS = 'Mausklick / Tippen'; const MULTI = 1; function run(wrap, W, H, cfg, onDone) { const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); const theme = cfg.theme || { primary: '#8b5cf6' }; const EMOJIS = cfg.emojis || ['🐱','🐶','🦊','🐸','🦋','🐠']; const all = [...EMOJIS, ...EMOJIS].sort(() => Math.random() - 0.5); let cards = all.map((e, i) => ({ e, i, open: false })); let flipped = [], matched = [], checking = false, moves = 0; let stopped = false, raf, startTime = null, elapsed = 0; // Layout: 4×3 const COLS = 4, ROWS = Math.ceil(all.length / 4); const PAD = 8; const cw = Math.floor((W - PAD * (COLS + 1)) / COLS); const ch = Math.floor((H - PAD * (ROWS + 2) - 24) / ROWS); const ox = (W - COLS * cw - PAD * (COLS - 1)) / 2; const oy = 30; function cardAt(mx, my) { for (let ri = 0; ri < ROWS; ri++) { for (let ci = 0; ci < COLS; ci++) { const idx = ri * COLS + ci; if (idx >= cards.length) continue; const x = ox + ci * (cw + PAD); const y = oy + ri * (ch + PAD); if (mx >= x && mx < x + cw && my >= y && my < y + ch) return cards[idx]; } } return null; } function onClick(e) { if (checking) return; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const mx = (e.clientX - rect.left) * scaleX; const my = (e.clientY - rect.top) * scaleY; const card = cardAt(mx, my); if (!card || card.open || matched.includes(card.i)) return; if (!startTime) startTime = performance.now(); card.open = true; flipped.push(card); if (flipped.length === 2) { moves++; checking = true; setTimeout(() => { if (flipped[0].e === flipped[1].e) { matched.push(flipped[0].i, flipped[1].i); if (matched.length === cards.length) { setTimeout(() => onDone(true), 600); } } else { flipped.forEach(c => c.open = false); } flipped = []; checking = false; }, 700); } } canvas.addEventListener('click', onClick); // Karten-Flip-Animation (progress 0–1) const flipAnim = new Map(); // card.i → { prog, dir } function loop(ts) { if (stopped) return; raf = requestAnimationFrame(loop); if (startTime) elapsed = (performance.now() - startTime) / 1000; ctx.fillStyle = '#050508'; ctx.fillRect(0, 0, W, H); // HUD MGAPI.text(ctx, `🃏 ${matched.length / 2} / ${EMOJIS.length} | Züge: ${moves}`, W / 2, 16, { size: 12, color: theme.primary }); // Karten zeichnen for (let ri = 0; ri < ROWS; ri++) { for (let ci = 0; ci < COLS; ci++) { const idx = ri * COLS + ci; if (idx >= cards.length) continue; const card = cards[idx]; const x = ox + ci * (cw + PAD); const y = oy + ri * (ch + PAD); const isM = matched.includes(card.i); const isOpen = card.open || isM; if (isM) { MGAPI.roundRect(ctx, x, y, cw, ch, 8, 'rgba(16,185,129,0.15)', '#10b981'); } else if (isOpen) { MGAPI.roundRect(ctx, x, y, cw, ch, 8, `${theme.primary}22`, theme.primary); } else { MGAPI.roundRect(ctx, x, y, cw, ch, 8, 'rgba(255,255,255,0.04)', 'rgba(255,255,255,0.1)'); } if (isOpen) { ctx.font = `${Math.min(cw, ch) * 0.55}px serif`; ctx.textAlign = 'center'; ctx.fillText(card.e, x + cw / 2, y + ch / 2 + ch * 0.15); } else { MGAPI.text(ctx, '?', x + cw / 2, y + ch / 2, { size: Math.floor(ch * 0.35), color: 'rgba(255,255,255,0.2)' }); } } } } raf = requestAnimationFrame(loop); return { stop() { stopped = true; cancelAnimationFrame(raf); canvas.removeEventListener('click', onClick); }, }; } function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, won => MGAPI.onResult(won)); } return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, launch, preview }; })();