/** * minigames/puzzle.js * 🧩 Zahlen-Puzzle — Schiebe die Kacheln in die richtige Reihenfolge! (15-Puzzle) */ window.MG_puzzle = (function() { const ID = 'puzzle'; const EMOJI = '🧩'; const NAME = 'Zahlen-Puzzle'; const DESC = 'Schiebe die Kacheln in die richtige Reihenfolge (1–8)!'; const CONTROLS = 'Mausklick / Pfeiltasten'; const MULTI = 1; function run(wrap, W, H, cfg, onDone) { const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); const theme = cfg.theme || { primary: '#14b8a6' }; const SIZE = 3; // 3×3 const PAD = 20; const cellW = Math.floor((W - PAD * 2) / SIZE); const cellH = Math.floor((H - PAD * 2 - 30) / SIZE); const OX = (W - SIZE * cellW) / 2; const OY = 30 + (H - 30 - SIZE * cellH) / 2; // Ziel: 1 2 3 / 4 5 6 / 7 8 _ const GOAL = [1,2,3,4,5,6,7,8,0]; let board = [...GOAL]; let moves = 0; let stopped = false; let raf, endTimer, solved = false; // Mischeln (100 zufällige Züge) function shuffle() { for (let i = 0; i < 100; i++) { const ei = board.indexOf(0); const er = Math.floor(ei / SIZE), ec = ei % SIZE; const nbr = []; if (er > 0) nbr.push(ei - SIZE); if (er < SIZE-1) nbr.push(ei + SIZE); if (ec > 0) nbr.push(ei - 1); if (ec < SIZE-1) nbr.push(ei + 1); const ni = nbr[Math.floor(Math.random() * nbr.length)]; [board[ei], board[ni]] = [board[ni], board[ei]]; } } shuffle(); function slideAt(idx) { if (solved) return; const ei = board.indexOf(0); const er = Math.floor(ei / SIZE), ec = ei % SIZE; const tr = Math.floor(idx / SIZE), tc = idx % SIZE; const adj = (er === tr && Math.abs(ec - tc) === 1) || (ec === tc && Math.abs(er - tr) === 1); if (!adj) return; [board[ei], board[idx]] = [board[idx], board[ei]]; moves++; if (board.join() === GOAL.join()) { solved = true; endTimer = setTimeout(() => onDone(true), 900); } } canvas.addEventListener('click', e => { const rect = canvas.getBoundingClientRect(); const mx = (e.clientX - rect.left) * (canvas.width / rect.width / (window.devicePixelRatio || 1)); const my = (e.clientY - rect.top) * (canvas.height / rect.height); for (let i = 0; i < SIZE * SIZE; i++) { const c = i % SIZE, r = Math.floor(i / SIZE); const x = OX + c * cellW, y = OY + r * cellH; if (mx >= x && mx < x + cellW - 4 && my >= y && my < y + cellH - 4) slideAt(i); } }); const onKey = e => { const ei = board.indexOf(0); const map = { ArrowUp: SIZE, ArrowDown: -SIZE, ArrowLeft: 1, ArrowRight: -1 }; const di = map[e.key]; if (di && ei + di >= 0 && ei + di < SIZE * SIZE) { e.preventDefault(); slideAt(ei + di); } }; document.addEventListener('keydown', onKey); function loop() { if (stopped) return; raf = requestAnimationFrame(loop); ctx.fillStyle = '#050508'; ctx.fillRect(0, 0, W, H); MGAPI.text(ctx, `🧩 Züge: ${moves}`, W / 2, 18, { size: 12, color: theme.primary }); for (let i = 0; i < SIZE * SIZE; i++) { const val = board[i]; const c = i % SIZE, r = Math.floor(i / SIZE); const x = OX + c * cellW, y = OY + r * cellH; if (val === 0) { // Leerfeld MGAPI.roundRect(ctx, x+2, y+2, cellW-8, cellH-8, 8, 'rgba(255,255,255,0.03)', null); continue; } const isCorrect = val === GOAL[i]; MGAPI.roundRect(ctx, x+2, y+2, cellW-8, cellH-8, 10, isCorrect ? `${theme.primary}33` : 'rgba(255,255,255,0.07)', isCorrect ? theme.primary : 'rgba(255,255,255,0.15)'); MGAPI.text(ctx, String(val), x + cellW/2 - 2, y + cellH/2 + 2, { size: Math.floor(cellH * 0.38), family: "'Fredoka One',cursive", color: isCorrect ? theme.primary : '#fff' }); } if (solved) MGAPI.resultScreen(ctx, W, H, true, `In ${moves} Zügen gelöst!`); } raf = requestAnimationFrame(loop); return { stop() { stopped = true; clearTimeout(endTimer); cancelAnimationFrame(raf); document.removeEventListener('keydown', onKey); }, }; } 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 }; })();