/** * minigames/maze.js * πŸŒ€ Labyrinth β€” Finde den Ausgang in der Zeit! */ window.MG_maze = (function() { const ID = 'maze'; const EMOJI = 'πŸŒ€'; const NAME = 'Labyrinth'; const DESC = '2 Leben, 15 Sekunden pro Versuch. Pfeiltasten zum Steuern!'; const CONTROLS = 'Pfeiltasten / WASD'; const MULTI = 3; function generateMaze(COLS, ROWS) { const cells = Array.from({ length: ROWS }, () => Array.from({ length: COLS }, () => ({ n: true, s: true, e: true, w: true, visited: false })) ); const stack = []; let cur = { c: 0, r: 0 }; cells[0][0].visited = true; stack.push(cur); while (stack.length) { const { c, r } = stack[stack.length - 1]; const neighbors = []; if (r > 0 && !cells[r-1][c].visited) neighbors.push({ c, r: r-1, dir: 'n' }); if (r < ROWS-1 && !cells[r+1][c].visited) neighbors.push({ c, r: r+1, dir: 's' }); if (c < COLS-1 && !cells[r][c+1].visited) neighbors.push({ c: c+1, r, dir: 'e' }); if (c > 0 && !cells[r][c-1].visited) neighbors.push({ c: c-1, r, dir: 'w' }); if (!neighbors.length) { stack.pop(); continue; } const next = neighbors[Math.floor(Math.random() * neighbors.length)]; cells[r][c][next.dir] = false; cells[next.r][next.c][{ n:'s', s:'n', e:'w', w:'e' }[next.dir]] = false; cells[next.r][next.c].visited = true; stack.push({ c: next.c, r: next.r }); } return cells; } function run(wrap, W, H, cfg, onDone) { const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); const theme = cfg.theme || { primary: '#6366f1' }; const MAX_LIVES = 2; const TIME_LIMIT = 15; const COLS = 9, ROWS = 7; const cellW = Math.floor((W - 16) / COLS); const cellH = Math.floor((H - 48) / ROWS); const OX = (W - COLS * cellW) / 2; const OY = 38; const WALL = 2; let maze = generateMaze(COLS, ROWS); let px = 0, py = 0; let lives = MAX_LIVES; let timeLeft = TIME_LIMIT; let startTs = null; let won = false; let gameOver = false; let stopped = false; let raf, endTimer; function resetRound(ts) { maze = generateMaze(COLS, ROWS); px = 0; py = 0; startTs = ts; timeLeft = TIME_LIMIT; } function tryMove(dc, dr) { if (won || gameOver) return; const cell = maze[py][px]; const dir = dc === 1 ? 'e' : dc === -1 ? 'w' : dr === 1 ? 's' : 'n'; if (cell[dir]) return; px += dc; py += dr; if (px === COLS - 1 && py === ROWS - 1 && !endTimer) { won = true; endTimer = setTimeout(() => onDone(true), 900); } } const held = {}; const onKey = e => { const map = { ArrowUp:[0,-1], ArrowDown:[0,1], ArrowLeft:[-1,0], ArrowRight:[1,0], w:[0,-1], s:[0,1], a:[-1,0], d:[1,0], }; const d = map[e.key]; if (!d) return; e.preventDefault(); if (e.type === 'keydown') { if (!held[e.key]) tryMove(d[0], d[1]); held[e.key] = true; } else held[e.key] = false; }; let touchStart = null; canvas.addEventListener('touchstart', e => { touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY }; }, { passive: true }); canvas.addEventListener('touchend', e => { if (!touchStart) return; const dx = e.changedTouches[0].clientX - touchStart.x; const dy = e.changedTouches[0].clientY - touchStart.y; Math.abs(dx) > Math.abs(dy) ? tryMove(dx > 0 ? 1 : -1, 0) : tryMove(0, dy > 0 ? 1 : -1); }, { passive: true }); document.addEventListener('keydown', onKey); document.addEventListener('keyup', onKey); function drawMaze() { for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { const x = OX + c * cellW, y = OY + r * cellH; const cell = maze[r][c]; ctx.strokeStyle = `${theme.primary}88`; ctx.lineWidth = WALL; if (cell.n && r === 0) { ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x+cellW,y); ctx.stroke(); } if (cell.w && c === 0) { ctx.beginPath(); ctx.moveTo(x,y); ctx.lineTo(x,y+cellH); ctx.stroke(); } if (cell.s) { ctx.beginPath(); ctx.moveTo(x,y+cellH); ctx.lineTo(x+cellW,y+cellH); ctx.stroke(); } if (cell.e) { ctx.beginPath(); ctx.moveTo(x+cellW,y); ctx.lineTo(x+cellW,y+cellH); ctx.stroke(); } } } } function loop(ts) { if (stopped) return; raf = requestAnimationFrame(loop); if (startTs === null) startTs = ts; // Timer if (!won && !gameOver) { timeLeft = Math.max(0, TIME_LIMIT - (ts - startTs) / 1000); if (timeLeft <= 0) { lives--; if (lives <= 0) { gameOver = true; if (!endTimer) endTimer = setTimeout(() => onDone(false), 1100); } else { resetRound(ts); } } } ctx.fillStyle = '#050508'; ctx.fillRect(0, 0, W, H); MGAPI.roundRect(ctx, OX, OY, cellW, cellH, 4, 'rgba(16,185,129,0.2)', null); MGAPI.roundRect(ctx, OX+(COLS-1)*cellW, OY+(ROWS-1)*cellH, cellW, cellH, 4, 'rgba(245,166,35,0.25)', null); drawMaze(); ctx.font = '14px serif'; ctx.textAlign = 'center'; ctx.fillText('🟒', OX + cellW/2, OY + cellH/2 + 5); ctx.fillText('🏁', OX + (COLS-0.5)*cellW, OY + (ROWS-0.5)*cellH + 5); // Spieler const plX = OX + px * cellW + cellW / 2; const plY = OY + py * cellH + cellH / 2; ctx.shadowColor = theme.primary; ctx.shadowBlur = 14; ctx.fillStyle = theme.primary; ctx.beginPath(); ctx.arc(plX, plY, Math.min(cellW, cellH) * 0.32, 0, Math.PI * 2); ctx.fill(); ctx.shadowBlur = 0; // HUD: Timer + Herzen const tSec = Math.ceil(timeLeft); const tColor = timeLeft < 5 ? '#ef4444' : timeLeft < 8 ? '#f59e0b' : theme.primary; MGAPI.text(ctx, `⏱ ${tSec}s`, W / 2, 16, { size: 13, color: tColor }); const hearts = '❀️'.repeat(lives) + 'πŸ–€'.repeat(Math.max(0, MAX_LIVES - lives)); MGAPI.text(ctx, hearts, W - 10, 16, { align: 'right', size: 12 }); MGAPI.text(ctx, '← ↑ β†’ ↓', 10, 16, { align: 'left', size: 10, color: 'rgba(255,255,255,0.3)' }); if (won) MGAPI.resultScreen(ctx, W, H, true, 'Ausgang gefunden! ⏱ ' + (TIME_LIMIT - timeLeft).toFixed(1) + 's'); if (gameOver) MGAPI.resultScreen(ctx, W, H, false, 'Zeit abgelaufen β€” nΓ€chstes Mal!'); } raf = requestAnimationFrame(loop); return { stop() { stopped = true; cancelAnimationFrame(raf); clearTimeout(endTimer); document.removeEventListener('keydown', onKey); document.removeEventListener('keyup', 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 }; })();