🧱 Spielchen gefällig? Tetris und Defender auf dem Dashboard

Hallo zusammen,

ich habe mal die „künstliche Inkompetenz“ herausgefordert, mit der Aufgabe: „kannst du für das lovelace-dashbord von home assistant eine karte erstellen, die wie das spiel tetris funktioniert“

Dabei ist das dabei herausgekommen

Der folgende Code muss als tetris.html im www - Verzeichnis abgelegt werden.

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>HA Tetris Pro - Final Layout</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <style>
        :root {
            --primary-bg: #101216;
            --card-bg: #1c2028;
            --text-color: #e1e1e1;
            --accent-color: #03a9f4;
            --danger-color: #f44336;
            --border-radius: 12px;
        }

        body {
            background-color: var(--primary-bg);
            color: var(--text-color);
            font-family: 'Roboto', sans-serif;
            margin: 0;
            display: flex;
            flex-direction: column;
            height: 100vh;
            width: 100vw;
            overflow: hidden;
        }

        #header {
            flex: 0 0 auto;
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 8px 15px;
            background-color: var(--card-bg);
            box-shadow: 0 2px 10px rgba(0,0,0,0.5);
            z-index: 20;
        }

        .stats-group { display: flex; gap: 12px; }
        .stat-box { font-weight: bold; font-size: 0.8em; line-height: 1.2; }
        .stat-box span { color: var(--accent-color); font-size: 1.1em; display: block; }

        #reset-btn {
            background-color: transparent;
            color: var(--danger-color);
            border: 1px solid var(--danger-color);
            border-radius: 6px;
            padding: 4px 8px;
            cursor: pointer;
            font-weight: bold;
            font-size: 0.75em;
        }

        /* Hauptcontainer mit verbesserter Platzverteilung */
        #main-content {
            flex: 1 1 auto;
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: flex-start; /* Startet oben */
            padding: 10px;
            min-height: 0;
            box-sizing: border-box;
        }

        #game-container {
            flex: 1 1 auto; /* Nimmt verfĂĽgbaren Platz ein */
            display: flex;
            align-items: center;
            justify-content: center;
            width: 100%;
            min-height: 0; /* Erlaubt dem Canvas zu schrumpfen */
            position: relative;
            margin-bottom: 10px; /* Sicherheitsabstand zu den Buttons */
        }

        canvas {
            max-height: 100%;
            max-width: 100%;
            aspect-ratio: 240 / 400;
            background-color: #000;
            border-radius: var(--border-radius);
            border: 2px solid #333;
            box-shadow: 0 4px 15px rgba(0,0,0,0.6);
            object-fit: contain;
        }

        #pause-overlay {
            position: absolute;
            top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.7);
            display: none;
            align-items: center;
            justify-content: center;
            font-size: 2em;
            font-weight: bold;
            color: var(--accent-color);
            border-radius: var(--border-radius);
            pointer-events: none;
        }

        /* Feste Steuerung am unteren Rand */
        #controls {
            flex: 0 0 auto;
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 8px;
            width: 100%;
            max-width: 300px;
            padding: 5px 0 15px 0;
            z-index: 20;
        }

        .btn {
            background-color: var(--card-bg);
            color: var(--text-color);
            border: 1px solid #444;
            border-radius: var(--border-radius);
            font-size: 1.3em;
            padding: 14px;
            text-align: center;
            cursor: pointer;
            user-select: none;
            box-shadow: 0 3px 5px rgba(0,0,0,0.3);
        }

        .btn:active { background-color: var(--accent-color); transform: scale(0.95); }
        .btn-rotate { grid-column: 2; grid-row: 1; border-color: var(--accent-color); color: var(--accent-color); }
        .btn-left { grid-column: 1; grid-row: 2; }
        .btn-down { grid-column: 2; grid-row: 2; }
        .btn-right { grid-column: 3; grid-row: 2; }

    </style>
</head>
<body>

    <div id="header">
        <div class="stats-group">
            <div class="stat-box">Score <span id="score">0</span></div>
            <div class="stat-box">Lvl <span id="level">1</span></div>
        </div>
        <button id="reset-btn" onclick="confirmReset()">NEU</button>
    </div>

    <div id="main-content">
        <div id="game-container" onclick="togglePause()">
            <canvas id="tetris" width="240" height="400"></canvas>
            <div id="pause-overlay">PAUSE</div>
        </div>

        <div id="controls">
            <div class="btn btn-rotate" onclick="playerRotate(1)">↻</div>
            <div class="btn btn-left" onclick="playerMove(-1)">â—€</div>
            <div class="btn btn-down" onclick="playerDrop()">â–Ľ</div>
            <div class="btn btn-right" onclick="playerMove(1)">â–¶</div>
        </div>
    </div>

    <script>
        const canvas = document.getElementById('tetris');
        const context = canvas.getContext('2d');
        const pauseOverlay = document.getElementById('pause-overlay');
        context.scale(20, 20);

        let paused = false;
        let dropCounter = 0;
        let dropInterval = 1000;
        let lastTime = 0;

        const colors = [null, '#FF4081', '#00E5FF', '#00E676', '#D500F9', '#FF9100', '#FFEA00', '#2979FF'];
        const arena = createMatrix(12, 20);
        const player = { pos: {x: 0, y: 0}, matrix: null, score: 0, lines: 0, level: 1 };

        function togglePause() {
            paused = !paused;
            pauseOverlay.style.display = paused ? 'flex' : 'none';
        }

        function confirmReset() {
            if (player.score === 0 || confirm("Neues Spiel starten?")) {
                arena.forEach(row => row.fill(0));
                player.score = 0; player.lines = 0; player.level = 1;
                dropInterval = 1000;
                paused = false;
                pauseOverlay.style.display = 'none';
                playerReset();
                updateScore();
            }
        }

        function arenaSweep() {
            let rowCount = 0;
            outer: for (let y = arena.length - 1; y > 0; --y) {
                for (let x = 0; x < arena[y].length; ++x) {
                    if (arena[y][x] === 0) continue outer;
                }
                const row = arena.splice(y, 1)[0].fill(0);
                arena.unshift(row);
                ++y; rowCount++;
            }
            if(rowCount > 0) {
                player.score += (rowCount * 10) * player.level;
                player.lines += rowCount;
                player.level = Math.floor(player.lines / 10) + 1;
                dropInterval = Math.max(100, 1000 - (player.level - 1) * 100);
                updateScore();
            }
        }

        function collide(arena, player) {
            const [m, o] = [player.matrix, player.pos];
            for (let y = 0; y < m.length; ++y) {
                for (let x = 0; x < m[y].length; ++x) {
                    if (m[y][x] !== 0 && (arena[y + o.y] && arena[y + o.y][x + o.x]) !== 0) return true;
                }
            }
            return false;
        }

        function createMatrix(w, h) {
            const matrix = [];
            while (h--) matrix.push(new Array(w).fill(0));
            return matrix;
        }

        function createPiece(type) {
            if (type === 'T') return [[0, 1, 0], [1, 1, 1], [0, 0, 0]];
            if (type === 'O') return [[2, 2], [2, 2]];
            if (type === 'L') return [[0, 3, 0], [0, 3, 0], [0, 3, 3]];
            if (type === 'J') return [[0, 4, 0], [0, 4, 0], [4, 4, 0]];
            if (type === 'I') return [[0, 5, 0, 0], [0, 5, 0, 0], [0, 5, 0, 0], [0, 5, 0, 0]];
            if (type === 'S') return [[0, 6, 6], [6, 6, 0], [0, 0, 0]];
            if (type === 'Z') return [[7, 7, 0], [0, 7, 7], [0, 0, 0]];
        }

        function draw() {
            context.fillStyle = '#000';
            context.fillRect(0, 0, canvas.width, canvas.height);
            // Raster
            context.lineWidth = 0.05;
            context.strokeStyle = 'rgba(255,255,255,0.1)';
            for (let x = 0; x <= 12; x++) { context.beginPath(); context.moveTo(x, 0); context.lineTo(x, 20); context.stroke(); }
            for (let y = 0; y <= 20; y++) { context.beginPath(); context.moveTo(0, y); context.lineTo(12, y); context.stroke(); }
            drawMatrix(arena, {x: 0, y: 0});
            drawMatrix(player.matrix, player.pos);
        }

        function drawMatrix(matrix, offset) {
            matrix.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value !== 0) {
                        context.fillStyle = colors[value];
                        context.beginPath();
                        context.roundRect(x + offset.x, y + offset.y, 0.95, 0.95, 0.1);
                        context.fill();
                    }
                });
            });
        }

        function merge(arena, player) {
            player.matrix.forEach((row, y) => {
                row.forEach((value, x) => {
                    if (value !== 0) arena[y + player.pos.y][x + player.pos.x] = value;
                });
            });
        }

        function playerDrop() {
            if (paused) return;
            player.pos.y++;
            if (collide(arena, player)) {
                player.pos.y--; merge(arena, player);
                playerReset(); arenaSweep();
            }
            dropCounter = 0;
        }

        function playerMove(dir) {
            if (paused) return;
            player.pos.x += dir;
            if (collide(arena, player)) player.pos.x -= dir;
        }

        function playerReset() {
            const pieces = 'ILJOTSZ';
            player.matrix = createPiece(pieces[pieces.length * Math.random() | 0]);
            player.pos.y = 0;
            player.pos.x = (arena[0].length / 2 | 0) - (player.matrix[0].length / 2 | 0);
            if (collide(arena, player)) {
                arena.forEach(row => row.fill(0));
                player.score = 0; player.lines = 0; player.level = 1;
                dropInterval = 1000; updateScore();
            }
        }

        function playerRotate(dir) {
            if (paused) return;
            const pos = player.pos.x;
            let offset = 1;
            rotate(player.matrix, dir);
            while (collide(arena, player)) {
                player.pos.x += offset;
                offset = -(offset + (offset > 0 ? 1 : -1));
                if (offset > player.matrix[0].length) {
                    rotate(player.matrix, -dir);
                    player.pos.x = pos; return;
                }
            }
        }

        function rotate(matrix, dir) {
            for (let y = 0; y < matrix.length; ++y) {
                for (let x = 0; x < y; ++x) {
                    [matrix[x][y], matrix[y][x]] = [matrix[y][x], matrix[x][y]];
                }
            }
            if (dir > 0) matrix.forEach(row => row.reverse());
            else matrix.reverse();
        }

        function update(time = 0) {
            const deltaTime = time - lastTime;
            lastTime = time;
            if (!paused) {
                dropCounter += deltaTime;
                if (dropCounter > dropInterval) playerDrop();
                draw();
            }
            requestAnimationFrame(update);
        }

        function updateScore() {
            document.getElementById('score').innerText = player.score;
            document.getElementById('level').innerText = player.level;
        }

        playerReset(); updateScore(); update();
    </script>
</body>
</html>

Der Code fĂĽr die Karte:

type: iframe
url: /local/tetris.html?v=4
aspect_ratio: 16:9

Es gibt auch eine Pause-Funktion, indem man in das Spielfeld klickt.

Hier noch ein paar Tipps:

  1. Voraussetzungen: Die Datei tetris.html muss im Ordner /config/www/ liegen , damit sie ĂĽber /local/tetris.html?v=4 erreichbar ist.
  2. Neustart-Hinweis: Einige Nutzer wissen evtl. nicht, dass man Home Assistant einmal neu starten muss, wenn man den www-Ordner zum ersten Mal erstellt hat.
  3. Anpassbarkeit: Man kann die Farben im :root-Bereich des CSS ganz einfach an das eigene Dashboard-Theme anpassen.

„Es gibt 1000 Dinge, die die Welt nicht braucht. Dieses hier ist wahrscheinlich eines davon.“

11 „Gefällt mir“

Da kommt das Kind im Mann wieder hervor! :wink: aber auf die Idee muss mal erst mal kommen! :+1:

4 „Gefällt mir“

Ich bin froh, dass es mit 69 noch so ist. :slightly_smiling_face:

5 „Gefällt mir“

Zugabe:

Ein weiterer Klassiker, mit dem ich so manche Mark in Spielautomaten versenkt habe.

# Defender :rocket:

Kann auch mit Cursor-Tasten gespielt werden.

7 „Gefällt mir“

Coole Idee, das probiere ich auf jeden Fall mal aus, auch wenn man es eigentlich nicht wirklich braucht :grinning_face: Defender finde ich sogar noch besser. :slight_smile:

Ich glaub, ihr habt einfach zu viel Zeit… ich sollte euch mal ein paar Aufgaben geben :rofl:

Aber mal im Ernst, finde ich immer wieder mega, wenn ihr „Alten“ solche Sachen teilt! Kenn auch genug im höheren Alter, die mit Technik gar nichts anfangen können und schon Probleme haben, den PC überhaupt einzuschalten. Daher haltet euch das!

5 „Gefällt mir“

Freut mich wenn’s gefällt.

Hier der Code fĂĽr Defender.

<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>HA Defender Pro - Keyboard Support</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <style>
        :root {
            --primary-bg: #050507;
            --card-bg: #1c2028;
            --accent-color: #03a9f4;
            --danger-color: #f44336;
            --ship-color: #e1e1e1;
            --laser-color: #00ff00;
            --ufo-eye: #ffeb3b;
            --text-color: #e1e1e1;
            --border-radius: 12px;
        }
        body {
            background-color: var(--primary-bg);
            color: var(--text-color);
            font-family: 'Roboto', sans-serif;
            margin: 0;
            display: flex;
            flex-direction: column;
            height: 100vh;
            overflow: hidden;
        }
        #header {
            flex: 0 0 auto;
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 10px 15px;
            background: var(--card-bg);
            box-shadow: 0 2px 10px rgba(0,0,0,0.5);
            z-index: 10;
        }
        #score-box { font-size: 1.2em; font-weight: bold; }
        #score { color: var(--accent-color); }
        
        #game-container {
            flex: 1 1 auto;
            position: relative;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 10px;
            min-height: 0;
            background: radial-gradient(circle at center, #1a1a22 0%, #050507 100%);
        }
        canvas {
            background: transparent;
            width: 100%;
            max-height: 100%;
            border: 2px solid #333;
            border-radius: var(--border-radius);
            touch-action: none;
            box-shadow: 0 0 20px rgba(3, 169, 244, 0.2);
        }
        
        #controls {
            flex: 0 0 auto;
            display: grid;
            grid-template-columns: 1fr 1fr 1fr;
            gap: 10px;
            padding: 15px;
            max-width: 400px;
            margin: 0 auto;
            z-index: 10;
        }
        .btn {
            background: var(--card-bg);
            color: white;
            border: 1px solid #444;
            padding: 15px;
            text-align: center;
            border-radius: var(--border-radius);
            font-size: 1.5em;
            user-select: none;
            cursor: pointer;
            box-shadow: 0 4px 6px rgba(0,0,0,0.3);
            transition: background 0.1s;
        }
        .btn:active { background: #333; }
        .btn-up:active { background-color: var(--accent-color); }
        .btn-down:active { background-color: var(--accent-color); }
        .btn-fire { background: var(--danger-color); border-color: #b71c1c; grid-column: 2; font-weight: bold;}
        .btn-fire:active { background: #b71c1c; }

        #game-over-msg {
            position: absolute;
            color: white;
            font-size: 3em;
            font-weight: bold;
            text-align: center;
            display: none;
            text-shadow: 0 0 10px var(--danger-color);
            pointer-events: none;
        }
    </style>
</head>
<body>
    <div id="header">
        <div id="score-box">Score: <span id="score">0</span></div>
        <button id="reset" style="background:none; border:1px solid #444; color:white; padding: 5px 10px; border-radius:4px; cursor:pointer;" onclick="resetGame()">Neu starten</button>
    </div>
    <div id="game-container">
        <canvas id="gameCanvas"></canvas>
        <div id="game-over-msg">GAME OVER</div>
    </div>
    <div id="controls">
        <div class="btn btn-up" onmousedown="moveUp()" onmouseup="stopMove()" ontouchstart="moveUp()" ontouchend="stopMove()">â–˛</div>
        <div class="btn btn-fire" onclick="fire()">FEUER</div>
        <div class="btn btn-down" onmousedown="moveDown()" onmouseup="stopMove()" ontouchstart="moveDown()" ontouchend="stopMove()">â–Ľ</div>
    </div>

<script>
    const canvas = document.getElementById('gameCanvas');
    const ctx = canvas.getContext('2d');
    const scoreEl = document.getElementById('score');
    const gameOverEl = document.getElementById('game-over-msg');

    canvas.width = 800;
    canvas.height = 400;

    let score = 0;
    let gameActive = true;
    let frameCount = 0;

    let ship = { x: 80, y: 200, w: 40, h: 18, dy: 0 };
    let bullets = [];
    let enemies = [];
    let stars = Array.from({length: 80}, () => ({
        x: Math.random() * 800, y: Math.random() * 400,
        s: Math.random() * 2 + 0.5, v: Math.random() * 0.5 + 0.5
    }));

    function getCol(name) { return getComputedStyle(document.documentElement).getPropertyValue(name).trim(); }

    // --- NEU: Tastatur Steuerung ---
    window.addEventListener('keydown', (e) => {
        if (!gameActive) return;
        if (e.key === 'ArrowUp') moveUp();
        if (e.key === 'ArrowDown') moveDown();
        if (e.key === ' ' || e.code === 'Space') {
            e.preventDefault(); // Verhindert Scrollen der Seite
            fire();
        }
    });

    window.addEventListener('keyup', (e) => {
        if (e.key === 'ArrowUp' || e.key === 'ArrowDown') stopMove();
    });

    // --- Funktionen ---
    function moveUp() { if(gameActive) ship.dy = -6; }
    function moveDown() { if(gameActive) ship.dy = 6; }
    function stopMove() { ship.dy = 0; }
    function fire() {
        if(!gameActive) return;
        bullets.push({ x: ship.x + ship.w - 5, y: ship.y + ship.h/2, w: 15, h: 2, speed: 12 });
    }

    function spawnEnemy() {
        if (Math.random() < 0.025 + (score/10000)) {
            enemies.push({ x: 800, y: Math.random() * 360 + 20, w: 30, h: 22, speed: 2.5 + Math.random() * 2, animFrame: Math.random() * 10 });
        }
    }

    function resetGame() {
        score = 0; scoreEl.innerText = score;
        ship.y = 200; ship.dy = 0;
        bullets = []; enemies = [];
        gameActive = true;
        gameOverEl.style.display = 'none';
    }

    function gameOver() { gameActive = false; gameOverEl.style.display = 'block'; }

    // --- Rendering ---
    function drawDefenderShip(ctx, s) {
        ctx.save();
        ctx.translate(s.x, s.y);
        if (gameActive && frameCount % 4 < 3) {
            ctx.fillStyle = (frameCount % 2 === 0) ? '#ff9800' : '#f44336';
            ctx.beginPath(); ctx.moveTo(-5, s.h/2 - 3); ctx.lineTo(-15 - (Math.random()*5), s.h/2); ctx.lineTo(-5, s.h/2 + 3); ctx.fill();
        }
        ctx.fillStyle = getCol('--ship-color');
        ctx.beginPath(); ctx.moveTo(0, s.h/2); ctx.lineTo(5, 0); ctx.lineTo(s.w - 10, 2); ctx.lineTo(s.w, s.h/2); ctx.lineTo(s.w - 10, s.h - 2); ctx.lineTo(5, s.h); ctx.closePath(); ctx.fill();
        ctx.fillStyle = getCol('--accent-color');
        ctx.beginPath(); ctx.moveTo(s.w - 15, 5); ctx.lineTo(s.w - 5, s.h/2); ctx.lineTo(s.w - 15, s.h - 5); ctx.fill();
        ctx.restore();
    }

    function drawUfoEnemy(ctx, e) {
        ctx.save();
        ctx.translate(e.x, e.y);
        const f = Math.floor((frameCount + e.animFrame) / 6) % 3;
        ctx.fillStyle = getCol('--danger-color');
        ctx.beginPath(); ctx.ellipse(e.w/2, e.h/2 - 2, e.w/2, e.h/3, 0, 0, Math.PI * 2); ctx.fill();
        ctx.fillStyle = '#ffcdd2';
        ctx.beginPath(); ctx.arc(e.w/2, e.h/2 - 4, e.w/4, Math.PI, 0); ctx.fill();
        if (frameCount % 10 < 8) { ctx.fillStyle = getCol('--ufo-eye'); ctx.fillRect(e.w/2 - 2, e.h/2 - 4, 4, 4); }
        ctx.strokeStyle = getCol('--danger-color'); ctx.lineWidth = 2; ctx.beginPath();
        ctx.moveTo(5, e.h - 5); ctx.lineTo(5 + (f===0?2:0), e.h);
        ctx.moveTo(e.w - 5, e.h - 5); ctx.lineTo(e.w - 5 - (f===1?2:0), e.h);
        ctx.moveTo(e.w/2, e.h - 4); ctx.lineTo(e.w/2, e.h - (f===2?0:3)); ctx.stroke();
        ctx.restore();
    }

    function update() {
        if (!gameActive) return;
        frameCount++;
        ship.y += ship.dy;
        if (ship.y < 20) ship.y = 20;
        if (ship.y > canvas.height - ship.h - 20) ship.y = canvas.height - ship.h - 20;
        bullets.forEach((b, i) => { b.x += b.speed; if (b.x > 800) bullets.splice(i, 1); });
        enemies.forEach((e, i) => {
            e.x -= e.speed;
            if (e.x < -40) enemies.splice(i, 1);
            if (ship.x < e.x + e.w && ship.x + ship.w > e.x && ship.y < e.y + e.h && ship.y + ship.h > e.y) gameOver();
            bullets.forEach((b, bi) => {
                if (b.x > e.x && b.x < e.x + e.w && b.y > e.y - 2 && b.y < e.y + e.h + 2) {
                    enemies.splice(i, 1); bullets.splice(bi, 1);
                    score += 100; scoreEl.innerText = score;
                }
            });
        });
        stars.forEach(s => { s.x -= s.v; if (s.x < 0) s.x = 800; });
        spawnEnemy();
    }

    function draw() {
        ctx.fillStyle = 'rgba(5, 5, 7, 0.3)';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        ctx.fillStyle = 'white';
        stars.forEach(s => ctx.fillRect(s.x, s.y, s.s, s.s));
        ctx.strokeStyle = getCol('--laser-color'); ctx.lineWidth = 2;
        bullets.forEach(b => { ctx.beginPath(); ctx.moveTo(b.x, b.y); ctx.lineTo(b.x + b.w, b.y); ctx.stroke(); });
        enemies.forEach(e => drawUfoEnemy(ctx, e));
        drawDefenderShip(ctx, ship);
    }

    function loop() { update(); draw(); requestAnimationFrame(loop); }
    resetGame(); loop();
</script>
</body>
</html>
7 „Gefällt mir“

Da geht 'ne Menge:
Space Invaders ____________________Pac Man

Asteroids __________________________Centipede

3 „Gefällt mir“

Geiler Scheixx :wink:

Der Pac Man Code wĂĽrde mich interessieren

Coole Idee… klappt nur nicht bei mir :slight_smile:

Wirklich lustig und klappt mit Tastatur einwandfrei. Leider bewegt sich auch der Bildschirm, wenn ich die Maustasten hoch/runter nutze. Muss wohl ein Spiel auswählen, wo ich rechts, links statt oben, unten klicken muss. :rofl:

Das ist schade. Läuft bei mir sogar auf’m Handy mit der Copmanion-App.
Evtl. tut sich ja was, wenn du aspect-ratio änderst- 16;9 - 5:4 oder so

Ich schaue mal, ob noch was geht.

1 „Gefällt mir“

Das war der Fehler → so geht es url: /local/tetris.html?v=4

1 „Gefällt mir“

Danke, jetzt geht es auch bei mir.

1 „Gefällt mir“
<!DOCTYPE html>
<html lang="de">
<head>
    <meta charset="UTF-8">
    <title>HA Pac-Man</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
    <style>
        :root {
            --bg-color: #050507;
            --wall-color: #2121ff;
            --pacman-color: #ffff00;
            --dot-color: #ffb8ae;
            --card-bg: #1c2028;
        }
        body {
            background-color: var(--bg-color);
            color: white;
            margin: 0;
            display: flex;
            flex-direction: column;
            height: 100vh;
            font-family: 'Courier New', Courier, monospace;
            overflow: hidden;
        }
        #header {
            flex: 0 0 auto;
            display: flex;
            justify-content: space-between;
            padding: 10px 20px;
            background: #111;
            border-bottom: 2px solid #333;
        }
        #game-container {
            flex: 1 1 auto;
            position: relative;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: 0;
        }
        canvas {
            background: #000;
            max-height: 100%;
            max-width: 100%;
            border: 2px solid #333;
            image-rendering: pixelated;
        }
        #controls {
            flex: 0 0 auto;
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 10px;
            padding: 15px;
            max-width: 300px;
            margin: 0 auto;
        }
        .btn {
            background: var(--card-bg);
            color: white;
            border: 1px solid #444;
            padding: 15px;
            text-align: center;
            border-radius: 8px;
            font-size: 1.5em;
            user-select: none;
        }
        .btn:active { background: #444; }
        .btn-up { grid-column: 2; }
        .btn-left { grid-column: 1; }
        .btn-down { grid-column: 2; }
        .btn-right { grid-column: 3; }
    </style>
</head>
<body>
    <div id="header">
        <div>SCORE: <span id="score">0</span></div>
        <button onclick="resetGame()" style="background:none; border:1px solid #444; color:white; border-radius:4px;">RESET</button>
    </div>
    <div id="game-container">
        <canvas id="pacmanCanvas"></canvas>
    </div>
    <div id="controls">
        <div class="btn btn-up" onclick="changeDir(0, -1)">â–˛</div>
        <div class="btn btn-left" onclick="changeDir(-1, 0)">â—€</div>
        <div class="btn btn-down" onclick="changeDir(0, 1)">â–Ľ</div>
        <div class="btn btn-right" onclick="changeDir(1, 0)">â–¶</div>
    </div>

<script>
    const canvas = document.getElementById('pacmanCanvas');
    const ctx = canvas.getContext('2d');
    const scoreEl = document.getElementById('score');

    const TILE_SIZE = 20;
    const map = [
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
        [1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1],
        [1,2,1,1,0,1,1,1,0,1,0,1,1,1,0,1,1,2,1],
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],
        [1,0,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,0,1],
        [1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,1],
        [1,1,1,1,0,1,1,1,0,1,0,1,1,1,0,1,1,1,1],
        [1,1,1,1,0,1,0,0,0,0,0,0,0,1,0,1,1,1,1],
        [1,1,1,1,0,1,0,1,1,0,1,1,0,1,0,1,1,1,1],
        [0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0],
        [1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1],
        [1,1,1,1,0,1,0,0,0,0,0,0,0,1,0,1,1,1,1],
        [1,1,1,1,0,1,0,1,1,1,1,1,0,1,0,1,1,1,1],
        [1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1],
        [1,0,1,1,0,1,1,1,0,1,0,1,1,1,0,1,1,0,1],
        [1,2,0,1,0,0,0,0,0,0,0,0,0,0,0,1,0,2,1],
        [1,1,0,1,0,1,0,1,1,1,1,1,0,1,0,1,0,1,1],
        [1,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,1],
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
    ];

    canvas.width = map[0].length * TILE_SIZE;
    canvas.height = map.length * TILE_SIZE;

    let score = 0;
    let powerMode = 0;
    let gameActive = true;

    const pacman = { x: 9, y: 15, dirX: 0, dirY: 0, nextDirX: 0, nextDirY: 0, anim: 0 };
    const ghosts = [
        { x: 9, y: 9, color: 'red', dirX: 1, dirY: 0 },
        { x: 8, y: 9, color: 'pink', dirX: -1, dirY: 0 },
        { x: 10, y: 9, color: 'cyan', dirX: 0, dirY: -1 }
    ];

    let dots = [];
    for(let y=0; y<map.length; y++) {
        for(let x=0; x<map[y].length; x++) {
            if(map[y][x] === 0 || map[y][x] === 2) dots.push({x, y, power: map[y][x] === 2, eaten: false});
        }
    }

    function changeDir(x, y) {
        pacman.nextDirX = x;
        pacman.nextDirY = y;
    }

    window.addEventListener('keydown', (e) => {
        if(e.key === 'ArrowUp') changeDir(0, -1);
        if(e.key === 'ArrowDown') changeDir(0, 1);
        if(e.key === 'ArrowLeft') changeDir(-1, 0);
        if(e.key === 'ArrowRight') changeDir(1, 0);
    });

    function update() {
        if(!gameActive) return;

        // Pacman Bewegung Logik
        if(Number.isInteger(pacman.x) && Number.isInteger(pacman.y)) {
            if(map[pacman.y + pacman.nextDirY]?.[pacman.x + pacman.nextDirX] !== 1) {
                pacman.dirX = pacman.nextDirX;
                pacman.dirY = pacman.nextDirY;
            }
            if(map[pacman.y + pacman.dirY]?.[pacman.x + pacman.dirX] === 1) {
                pacman.dirX = 0; pacman.dirY = 0;
            }
        }

        pacman.x += pacman.dirX * 0.1;
        pacman.y += pacman.dirY * 0.1;
        pacman.x = Math.round(pacman.x * 10) / 10;
        pacman.y = Math.round(pacman.y * 10) / 10;

        // Tunnel
        if(pacman.x < 0) pacman.x = map[0].length - 1;
        if(pacman.x > map[0].length - 1) pacman.x = 0;

        // Essen
        dots.forEach(d => {
            if(!d.eaten && Math.abs(pacman.x - d.x) < 0.5 && Math.abs(pacman.y - d.y) < 0.5) {
                d.eaten = true;
                score += d.power ? 50 : 10;
                if(d.power) powerMode = 500;
                scoreEl.innerText = score;
            }
        });

        if(powerMode > 0) powerMode--;

        // Geister
        ghosts.forEach(g => {
            if(Number.isInteger(g.x) && Number.isInteger(g.y)) {
                const dirs = [[0,1], [0,-1], [1,0], [-1,0]].filter(d => map[g.y+d[1]]?.[g.x+d[0]] !== 1 && (d[0] !== -g.dirX || d[1] !== -g.dirY));
                const dir = dirs[Math.floor(Math.random() * dirs.length)];
                if(dir) { g.dirX = dir[0]; g.dirY = dir[1]; }
            }
            g.x += g.dirX * 0.08;
            g.y += g.dirY * 0.08;
            g.x = Math.round(g.x * 100) / 100;
            g.y = Math.round(g.y * 100) / 100;

            if(Math.abs(pacman.x - g.x) < 0.6 && Math.abs(pacman.y - g.y) < 0.6) {
                if(powerMode > 0) { g.x = 9; g.y = 9; score += 200; }
                else gameActive = false;
            }
        });
    }

    function draw() {
        ctx.fillStyle = 'black';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        // Map
        for(let y=0; y<map.length; y++) {
            for(let x=0; x<map[y].length; x++) {
                if(map[y][x] === 1) {
                    ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--wall-color');
                    ctx.fillRect(x*TILE_SIZE+2, y*TILE_SIZE+2, TILE_SIZE-4, TILE_SIZE-4);
                }
            }
        }

        // Dots
        dots.forEach(d => {
            if(!d.eaten) {
                ctx.fillStyle = d.power ? 'white' : getComputedStyle(document.documentElement).getPropertyValue('--dot-color');
                ctx.beginPath();
                ctx.arc(d.x*TILE_SIZE+TILE_SIZE/2, d.y*TILE_SIZE+TILE_SIZE/2, d.power ? 6 : 2, 0, Math.PI*2);
                ctx.fill();
            }
        });

        // Pacman
        ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--pacman-color');
        ctx.beginPath();
        ctx.arc(pacman.x*TILE_SIZE+TILE_SIZE/2, pacman.y*TILE_SIZE+TILE_SIZE/2, TILE_SIZE/2-2, 0.2 * Math.PI, 1.8 * Math.PI);
        ctx.lineTo(pacman.x*TILE_SIZE+TILE_SIZE/2, pacman.y*TILE_SIZE+TILE_SIZE/2);
        ctx.fill();

        // Geister
        ghosts.forEach(g => {
            ctx.fillStyle = powerMode > 0 ? (powerMode < 100 && frameCount % 20 < 10 ? 'white' : 'blue') : g.color;
            ctx.beginPath();
            ctx.arc(g.x*TILE_SIZE+TILE_SIZE/2, g.y*TILE_SIZE+TILE_SIZE/2, TILE_SIZE/2-2, Math.PI, 0);
            ctx.lineTo(g.x*TILE_SIZE+TILE_SIZE-2, g.y*TILE_SIZE+TILE_SIZE-2);
            ctx.lineTo(g.x*TILE_SIZE+2, g.y*TILE_SIZE+TILE_SIZE-2);
            ctx.fill();
        });

        if(!gameActive) {
            ctx.fillStyle = 'white';
            ctx.font = '24px Courier';
            ctx.textAlign = 'center';
            ctx.fillText('GAME OVER', canvas.width/2, canvas.height/2);
        }
    }

    let frameCount = 0;
    function loop() {
        frameCount++;
        update();
        draw();
        requestAnimationFrame(loop);
    }
    loop();

    function resetGame() {
        pacman.x = 9; pacman.y = 15; pacman.dirX = 0; pacman.dirY = 0;
        dots.forEach(d => d.eaten = false);
        score = 0; scoreEl.innerText = score;
        gameActive = true;
        powerMode = 0;
        ghosts.forEach((g, i) => { g.x = 8+i; g.y = 9; });
    }
</script>
</body>
</html>
1 „Gefällt mir“