/** * minigames/snake2p.js * 🐍🐍 Snake-Duell β€” 2-Spieler Best-of-3 im Stil von snake.js * Blau (Pfeiltasten) startet rechts Β· Rot (WASD) startet links. * Ruft MGAPI.onResult(winnerIdx) mit 0 (P1), 1 (P2) oder -1 (Unentschieden). */ window.MG_snake2p = (function () { const ID = 'snake2p'; const EMOJI = '🐍🐍'; const NAME = 'Snake-Duell'; const DESC = 'Best-of-3 Β· πŸ”΅ Pfeile (rechts) gegen πŸ”΄ WASD (links). 2 Γ„pfel auf dem Feld.'; const CONTROLS = '← ↑ β†’ ↓ / W A S D'; const MULTI = 1; const REQUIRES = '2p'; const TOTAL_ROUNDS = 3; const STEP_INTERVAL = 160; // wie snake.js Start-Tempo, konstant (fair fΓΌrs Duell) const P1_COL = '#3b82f6'; // Pfeiltasten, startet rechts const P2_COL = '#ef4444'; // WASD, startet links const APPLE_COL = '#f59e0b'; function run(wrap, W, H, cfg, onDone) { const { canvas, ctx } = MGAPI.makeCanvas(wrap, W, H); const SZ = 20; const COLS = Math.floor(W / SZ); const ROWS = Math.floor(H / SZ); const scores = [0, 0]; // [P1 (Pfeile), P2 (WASD)] let roundIdx = 0; let state = 'intro'; // intro | playing | roundEnd | duelEnd let stateTimer = 0; let lastTs = 0; let lastStepTs = 0; let stopped = false, raf = null; let roundResult = null; let startBtnRect = null; let s1 = null, s2 = null; let apples = []; // ── Snake-Init ── function startSnakeRight() { // P1 (Pfeiltasten), Blau, startet rechts und zieht nach links const my = Math.floor(ROWS / 2); const x0 = Math.max(5, COLS - 6); return { body: [{x:x0, y:my}, {x:x0+1, y:my}, {x:x0+2, y:my}], dir:{x:-1,y:0}, nextDir:{x:-1,y:0}, alive:true }; } function startSnakeLeft() { // P2 (WASD), Rot, startet links und zieht nach rechts const my = Math.floor(ROWS / 2); const x0 = Math.min(COLS - 6, 5); return { body: [{x:x0, y:my}, {x:x0-1, y:my}, {x:x0-2, y:my}], dir:{x:1,y:0}, nextDir:{x:1,y:0}, alive:true }; } function rndApple() { let safety = 200; while (safety-- > 0) { const f = { x: Math.floor(Math.random()*COLS), y: Math.floor(Math.random()*ROWS) }; if (s1.body.some(c=>c.x===f.x&&c.y===f.y)) continue; if (s2.body.some(c=>c.x===f.x&&c.y===f.y)) continue; if (apples.some(a=>a.x===f.x&&a.y===f.y)) continue; return f; } return null; } function spawnApples() { while (apples.length < 2) { const a = rndApple(); if (!a) break; apples.push(a); } } function newRound() { s1 = startSnakeRight(); s2 = startSnakeLeft(); apples = []; spawnApples(); lastStepTs = 0; roundResult = null; state = 'playing'; stateTimer = 0; } function endRound(winner) { if (winner === 0 || winner === 1) scores[winner]++; roundResult = winner; state = 'roundEnd'; stateTimer = 0; } function advanceRound() { roundIdx++; if (scores[0] >= 2 || scores[1] >= 2 || roundIdx >= TOTAL_ROUNDS) { state = 'duelEnd'; stateTimer = 0; } else { newRound(); } } function finalWinner() { if (scores[0] > scores[1]) return 0; if (scores[1] > scores[0]) return 1; return -1; } function finishUp() { if (stopped) return; stopped = true; cancelAnimationFrame(raf); document.removeEventListener('keydown', onKey); canvas.removeEventListener('click', onClick); canvas.removeEventListener('touchend', onTouch); onDone(finalWinner()); } // ── Steuerung ── function setDir(s, dx, dy) { if (s.dir.x === -dx && s.dir.y === -dy) return; // kein 180Β° s.nextDir = { x: dx, y: dy }; } function onKey(e) { if (state !== 'playing') return; const k = e.key; let handled = true; if (k === 'ArrowUp') setDir(s1, 0, -1); else if (k === 'ArrowDown') setDir(s1, 0, 1); else if (k === 'ArrowLeft') setDir(s1, -1, 0); else if (k === 'ArrowRight') setDir(s1, 1, 0); else if (k === 'w' || k === 'W') setDir(s2, 0, -1); else if (k === 's' || k === 'S') setDir(s2, 0, 1); else if (k === 'a' || k === 'A') setDir(s2, -1, 0); else if (k === 'd' || k === 'D') setDir(s2, 1, 0); else handled = false; if (handled) e.preventDefault(); } function pointInBtn(x, y) { return startBtnRect && x >= startBtnRect.x && x <= startBtnRect.x + startBtnRect.w && y >= startBtnRect.y && y <= startBtnRect.y + startBtnRect.h; } function canvasCoords(clientX, clientY) { const r = canvas.getBoundingClientRect(); const dpr = window.devicePixelRatio || 1; const x = (clientX - r.left) * (canvas.width / r.width / dpr); const y = (clientY - r.top) * (canvas.height / r.height / dpr); return { x, y }; } function onClick(e) { if (state !== 'intro') return; const p = canvasCoords(e.clientX, e.clientY); if (pointInBtn(p.x, p.y)) newRound(); } function onTouch(e) { if (state !== 'intro') return; const t = e.changedTouches && e.changedTouches[0]; if (!t) return; const p = canvasCoords(t.clientX, t.clientY); if (pointInBtn(p.x, p.y)) { e.preventDefault(); newRound(); } } document.addEventListener('keydown', onKey); canvas.addEventListener('click', onClick); canvas.addEventListener('touchend', onTouch, { passive: false }); // ── Spielschritt ── function stepGame() { s1.dir = s1.nextDir; s2.dir = s2.nextDir; const h1 = { x: s1.body[0].x + s1.dir.x, y: s1.body[0].y + s1.dir.y }; const h2 = { x: s2.body[0].x + s2.dir.x, y: s2.body[0].y + s2.dir.y }; const out1 = h1.x<0||h1.x>=COLS||h1.y<0||h1.y>=ROWS; const out2 = h2.x<0||h2.x>=COLS||h2.y<0||h2.y>=ROWS; const self1 = s1.body.some(c=>c.x===h1.x&&c.y===h1.y); const self2 = s2.body.some(c=>c.x===h2.x&&c.y===h2.y); const cross1 = s2.body.some(c=>c.x===h1.x&&c.y===h1.y); const cross2 = s1.body.some(c=>c.x===h2.x&&c.y===h2.y); const headOn = h1.x===h2.x && h1.y===h2.y; let die1 = out1||self1||cross1; let die2 = out2||self2||cross2; if (headOn) { die1 = true; die2 = true; } if (die1) s1.alive = false; if (die2) s2.alive = false; if (s1.alive) { s1.body.unshift(h1); const ai = apples.findIndex(a=>a.x===h1.x&&a.y===h1.y); if (ai >= 0) apples.splice(ai,1); else s1.body.pop(); } if (s2.alive) { s2.body.unshift(h2); const ai = apples.findIndex(a=>a.x===h2.x&&a.y===h2.y); if (ai >= 0) apples.splice(ai,1); else s2.body.pop(); } spawnApples(); if (!s1.alive || !s2.alive) { let w; if (!s1.alive && !s2.alive) w = -1; else if (!s1.alive) w = 1; else w = 0; endRound(w); } } // ── Render (gleicher Stil wie snake.js) ── function drawGrid() { ctx.strokeStyle = 'rgba(255,255,255,0.04)'; ctx.lineWidth = 0.5; for (let x = 0; x < COLS; x++) for (let y = 0; y < ROWS; y++) ctx.strokeRect(x*SZ, y*SZ, SZ, SZ); } function drawSnake(s, col) { s.body.forEach((c, i) => { const alpha = i === 0 ? 'ff' : Math.max(0x44, 0x88 - i*2).toString(16).padStart(2, '0'); const fill = i === 0 ? col : `${col}${alpha}`; if (i === 0) { ctx.shadowColor = col; ctx.shadowBlur = 8; } MGAPI.roundRect(ctx, c.x*SZ + 2, c.y*SZ + 2, SZ-4, SZ-4, 4, fill, null); ctx.shadowBlur = 0; }); } function drawApples() { ctx.shadowColor = APPLE_COL; ctx.shadowBlur = 12; ctx.font = `${SZ-2}px serif`; ctx.textAlign = 'center'; apples.forEach(a => ctx.fillText('🍎', a.x*SZ + SZ/2, a.y*SZ + SZ/1.2)); ctx.shadowBlur = 0; } function drawHUD() { MGAPI.text(ctx, `πŸ”΅ ${scores[0]}`, 8, 14, { align:'left', size:13, color:P1_COL }); MGAPI.text(ctx, `Runde ${Math.min(roundIdx+1, TOTAL_ROUNDS)} / ${TOTAL_ROUNDS}`, W/2, 14, { align:'center', size:12, color:'rgba(255,255,255,0.85)' }); MGAPI.text(ctx, `${scores[1]} πŸ”΄`, W-8, 14, { align:'right', size:13, color:P2_COL }); } function drawStartButton() { const bw = Math.min(320, W*0.62), bh = 56; const bx = (W - bw)/2, by = H/2 + 8; startBtnRect = { x: bx, y: by, w: bw, h: bh }; // Schatten ctx.fillStyle = 'rgba(0,0,0,0.45)'; MGAPI.roundRect(ctx, bx, by+4, bw, bh, 14, 'rgba(0,0,0,0.45)', null); // Button-Gradient const grd = ctx.createLinearGradient(0, by, 0, by+bh); grd.addColorStop(0, '#7c3aed'); grd.addColorStop(1, '#5b21b6'); MGAPI.roundRect(ctx, bx, by, bw, bh, 14, grd, null); // Glanz oben MGAPI.roundRect(ctx, bx+4, by+3, bw-8, bh/2 - 4, 12, 'rgba(255,255,255,0.18)', null); // Text MGAPI.text(ctx, 'β–Ά Duell starten', bx + bw/2, by + bh/2, { size: 22, color:'#fff', weight:'bold', family: "'Fredoka One',cursive" }); } function drawOverlayText(title, sub, color) { ctx.fillStyle = 'rgba(0,0,0,0.62)'; ctx.fillRect(0, 0, W, H); MGAPI.text(ctx, title, W/2, H/2 - 20, { size: 28, weight:'bold', color: color||'#fff', family: "'Fredoka One',cursive", shadow:'rgba(0,0,0,0.7)', shadowBlur: 12 }); if (sub) MGAPI.text(ctx, sub, W/2, H/2 + 18, { size: 15, color:'rgba(255,255,255,0.88)' }); } function render() { // Hintergrund + Grid (wie snake.js) ctx.fillStyle = '#050508'; ctx.fillRect(0, 0, W, H); drawGrid(); if (state !== 'intro') { drawApples(); if (s1) drawSnake(s1, s1.alive ? P1_COL : 'rgba(80,80,120,0.5)'); if (s2) drawSnake(s2, s2.alive ? P2_COL : 'rgba(80,80,120,0.5)'); } drawHUD(); if (state === 'intro') { MGAPI.text(ctx, 'Snake-Duell Β· Best of 3', W/2, H/2 - 56, { size: 26, weight:'bold', color:'#fff', family:"'Fredoka One',cursive", shadow:'rgba(0,0,0,0.5)', shadowBlur: 10 }); MGAPI.text(ctx, 'πŸ”΅ Pfeiltasten (Start rechts) Β· πŸ”΄ WASD (Start links)', W/2, H/2 - 24, { size: 13, color:'rgba(255,255,255,0.85)' }); drawStartButton(); } else if (state === 'roundEnd') { let t, c; if (roundResult === 0) { t = 'πŸ”΅ Runde fΓΌr Spieler 1!'; c = '#93c5fd'; } else if (roundResult === 1) { t = 'πŸ”΄ Runde fΓΌr Spieler 2!'; c = '#fca5a5'; } else { t = 'βš–οΈ Unentschieden!'; c = '#fde68a'; } drawOverlayText(t, `Stand: πŸ”΅ ${scores[0]} : ${scores[1]} πŸ”΄`, c); } else if (state === 'duelEnd') { const w = finalWinner(); let t, c; if (w === 0) { t = 'πŸ† Spieler 1 gewinnt!'; c = '#93c5fd'; } else if (w === 1) { t = 'πŸ† Spieler 2 gewinnt!'; c = '#fca5a5'; } else { t = '🀝 Unentschieden!'; c = '#fde68a'; } drawOverlayText(t, `Endstand: πŸ”΅ ${scores[0]} : ${scores[1]} πŸ”΄`, c); } } function loop(ts) { if (stopped) return; raf = requestAnimationFrame(loop); if (!lastTs) lastTs = ts; const dt = ts - lastTs; lastTs = ts; if (state === 'playing') { if (!lastStepTs) lastStepTs = ts; if (ts - lastStepTs >= STEP_INTERVAL) { lastStepTs = ts; stepGame(); } } else { stateTimer += dt; if (state === 'roundEnd' && stateTimer >= 1800) advanceRound(); else if (state === 'duelEnd' && stateTimer >= 2200) finishUp(); // intro: wartet auf Klick auf den Start-Button } render(); } raf = requestAnimationFrame(loop); return { stop() { if (stopped) return; stopped = true; cancelAnimationFrame(raf); document.removeEventListener('keydown', onKey); canvas.removeEventListener('click', onClick); canvas.removeEventListener('touchend', onTouch); }, }; } function launch(wrap, W, H, cfg) { return run(wrap, W, H, cfg, idx => MGAPI.onResult(idx)); } function preview(wrap, W, H, cfg) { return run(wrap, W, H, cfg, idx => MGAPI.onResult(idx)); } return { id: ID, emoji: EMOJI, name: NAME, desc: DESC, controls: CONTROLS, multi: MULTI, requires: REQUIRES, launch, preview }; })();