JIT
Grid - Untitled
Self-playing side-view pinball in a single HTML file. Pure CSS/JS, no assets. Features chrome ball physics, shadows, screen shake, and flashing bumpers. Auto-flippers play through 3 balls, updating the score until a 'GAME OVER' fade.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Pinball</title> <style> body, html { margin: 0; padding: 0; overflow: hidden; height: 100vh; width: 100vw; background: #000; display: flex; justify-content: center; align-items: center; font-family: sans-serif; } #game-container { position: relative; width: 100vw; height: 100vh; max-width: 1512px; max-height: 982px; aspect-ratio: 1512 / 982; background: linear-gradient(to bottom, #222, #111); overflow: hidden; box-sizing: border-box; border: 2px solid #444; transform-origin: center; } #table { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: transparent; } .wall { position: absolute; background: #333; box-shadow: 0 0 10px rgba(0,0,0,0.5); } #left-wall { left: 0; top: 0; width: 2%; height: 100%; } #right-wall { right: 0; top: 0; width: 2%; height: 100%; } #top-wall { left: 0; top: 0; width: 100%; height: 2%; } #bottom-drain { left: 40%; bottom: 0; width: 20%; height: 2%; background: #000; } .bumper { position: absolute; width: 5vw; height: 5vw; border-radius: 50%; background: radial-gradient(circle, #f00, #a00); box-shadow: 0 0 15px rgba(255,0,0,0.5); } .bumper.flash { animation: flash 0.5s; } @keyframes flash { 0% { background: radial-gradient(circle, #ff0, #aa0); box-shadow: 0 0 30px #ff0; } 100% { background: radial-gradient(circle, #f00, #a00); box-shadow: 0 0 15px rgba(255,0,0,0.5); } } #bumper1 { top: 20%; left: 30%; } #bumper2 { top: 20%; right: 30%; } #bumper3 { top: 40%; left: 45%; } .flipper { position: absolute; bottom: 5%; width: 15vw; height: 2vw; background: #ccc; transform-origin: left center; transition: transform 0.1s; box-shadow: 0 5px 10px rgba(0,0,0,0.5); } .flipper.active { transform: rotate(-45deg); } #left-flipper { left: 20%; transform-origin: left center; } #right-flipper { right: 20%; transform-origin: right center; } #right-flipper.active { transform: rotate(45deg); } #ball { position: absolute; width: 3vw; height: 3vw; border-radius: 50%; background: radial-gradient(circle at 30% 30%, #fff, #aaa, #555); box-shadow: 0 0 10px rgba(255,255,255,0.3), 5px 5px 10px rgba(0,0,0,0.5); } #score { position: absolute; top: 5%; left: 50%; transform: translateX(-50%); color: #fff; font-size: 4vw; text-shadow: 0 0 10px #000; } #balls-left { position: absolute; top: 10%; left: 50%; transform: translateX(-50%); color: #fff; font-size: 2vw; } #game-over { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #f00; font-size: 10vw; opacity: 0; transition: opacity 1s; text-shadow: 0 0 20px #000; } #game-over.visible { opacity: 1; } @keyframes shake { 0% { transform: translate(0, 0) rotate(0); } 10% { transform: translate(-1%, 0) rotate(-1deg); } 20% { transform: translate(1%, 0) rotate(1deg); } 30% { transform: translate(0, 1%) rotate(0); } 40% { transform: translate(1%, -1%) rotate(1deg); } 50% { transform: translate(-1%, 1%) rotate(-1deg); } 60% { transform: translate(-1%, 0) rotate(0); } 70% { transform: translate(1%, 0) rotate(0); } 80% { transform: translate(-1%, -1%) rotate(-1deg); } 90% { transform: translate(1%, 1%) rotate(1deg); } 100% { transform: translate(0, 0) rotate(0); } } .shake { animation: shake 0.5s; } </style> </head> <body> <div id="game-container"> <div id="table"> <div id="left-wall" class="wall"></div> <div id="right-wall" class="wall"></div> <div id="top-wall" class="wall"></div> <div id="bottom-drain"></div> <div id="bumper1" class="bumper"></div> <div id="bumper2" class="bumper"></div> <div id="bumper3" class="bumper"></div> <div id="left-flipper" class="flipper"></div> <div id="right-flipper" class="flipper"></div> <div id="ball"></div> </div> <div id="score">Score: 0</div> <div id="balls-left">Balls: 3</div> <div id="game-over">GAME OVER</div> </div> <script> const container = document.getElementById('game-container'); const ball = document.getElementById('ball'); const leftFlipper = document.getElementById('left-flipper'); const rightFlipper = document.getElementById('right-flipper'); const scoreEl = document.getElementById('score'); const ballsLeftEl = document.getElementById('balls-left'); const gameOverEl = document.getElementById('game-over'); const bumpers = document.querySelectorAll('.bumper');
    let ballX = container.clientWidth * 0.9;
    let ballY = container.clientHeight * 0.1;
    let velX = -2;
    let velY = 0;
    let gravity = 0.5;
    let friction = 0.98;
    let score = 0;
    let balls = 3;
    let gameOver = false;

    ball.style.left = `${ballX}px`;
    ball.style.top = `${ballY}px`;

    function launchBall() {
        ballX = container.clientWidth * 0.9;
        ballY = container.clientHeight * 0.1;
        velX = -Math.random() * 2 - 1;
        velY = Math.random() * 2 + 1;
        ball.style.left = `${ballX}px`;
        ball.style.top = `${ballY}px`;
    }

    function checkCollision(el1, el2) {
        const rect1 = el1.getBoundingClientRect();
        const rect2 = el2.getBoundingClientRect();
        return !(rect1.right < rect2.left || rect1.left > rect2.right || rect1.bottom < rect2.top || rect1.top > rect2.bottom);
    }

    function bounceOff(el, isFlipper = false) {
        velY = -Math.abs(velY) * (isFlipper ? 1.5 : 1.2);
        velX += (Math.random() - 0.5) * 2;
        if (isFlipper) score += 10;
        else score += 50;
        container.classList.add('shake');
        setTimeout(() => container.classList.remove('shake'), 500);
    }

    function update() {
        if (gameOver) return;

        velY += gravity;
        velX *= friction;
        velY *= friction;
        ballX += velX;
        ballY += velY;

        // Walls
        if (ballX <= container.clientWidth * 0.02 || ballX >= container.clientWidth * 0.98 - ball.clientWidth) {
            velX = -velX * 0.8;
            ballX = Math.max(container.clientWidth * 0.02, Math.min(ballX, container.clientWidth * 0.98 - ball.clientWidth));
        }
        if (ballY <= container.clientHeight * 0.02) {
            velY = -velY * 0.8;
            ballY = container.clientHeight * 0.02;
        }

        // Drain
        if (ballY >= container.clientHeight - ball.clientHeight && ballX > container.clientWidth * 0.4 && ballX < container.clientWidth * 0.6) {
            balls--;
            ballsLeftEl.textContent = `Balls: ${balls}`;
            if (balls > 0) {
                launchBall();
            } else {
                gameOver = true;
                gameOverEl.classList.add('visible');
            }
            return;
        }

        // Bumpers
        bumpers.forEach(bumper => {
            if (checkCollision(ball, bumper)) {
                bounceOff(bumper);
                bumper.classList.add('flash');
                setTimeout(() => bumper.classList.remove('flash'), 500);
            }
        });

        // Auto-flippers
        const flipZone = container.clientHeight * 0.8;
        if (ballY > flipZone) {
            if (ballX < container.clientWidth / 2) {
                leftFlipper.classList.add('active');
                setTimeout(() => leftFlipper.classList.remove('active'), 200);
                if (checkCollision(ball, leftFlipper)) bounceOff(leftFlipper, true);
            } else {
                rightFlipper.classList.add('active');
                setTimeout(() => rightFlipper.classList.remove('active'), 200);
                if (checkCollision(ball, rightFlipper)) bounceOff(rightFlipper, true);
            }
        }

        ball.style.left = `${ballX}px`;
        ball.style.top = `${ballY}px`;
        scoreEl.textContent = `Score: ${score}`;

        requestAnimationFrame(update);
    }

    launchBall();
    update();
</script>
</body> </html>*
Updated files
v1