194 lines
6.9 KiB
JavaScript
194 lines
6.9 KiB
JavaScript
/**
|
|
* 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 };
|
|
|
|
})();
|