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>Auto-Play Pinball</title> <style> :root { --neon-blue: #0ff; --neon-pink: #f0f; --neon-orange: #f80; --bg-color: #111; --table-bg: #1a1a1a; }
    body {
        margin: 0;
        padding: 0;
        width: 100vw;
        height: 100vh;
        background-color: #050505;
        color: white;
        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
        overflow: hidden;
        display: flex;
        justify-content: center;
        align-items: center;
    }

    #game-container {
        position: relative;
        width: 60vh; /* Aspect ratio roughly controlled by height */
        height: 90vh;
        max-width: 600px;
        box-shadow: 0 0 50px rgba(0, 0, 0, 0.8), 
                    0 0 20px rgba(0, 255, 255, 0.1) inset;
        border-radius: 20px 20px 0 0;
        background: var(--table-bg);
        border: 8px solid #333;
        border-bottom: none;
        overflow: hidden;
    }

    canvas {
        display: block;
        width: 100%;
        height: 100%;
    }

    .ui-layer {
        position: absolute;
        top: 20px;
        left: 0;
        width: 100%;
        text-align: center;
        pointer-events: none;
        z-index: 10;
        text-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
    }

    .score-display {
        font-size: 3rem;
        font-weight: 800;
        letter-spacing: 2px;
        color: #fff;
        margin-bottom: 5px;
    }

    .ball-display {
        font-size: 1.2rem;
        color: #aaa;
        text-transform: uppercase;
        letter-spacing: 1px;
    }

    .game-over-overlay {
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0, 0, 0, 0.9);
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
        opacity: 0;
        pointer-events: none;
        transition: opacity 2s ease;
        z-index: 20;
    }

    .game-over-overlay.visible {
        opacity: 1;
        pointer-events: auto;
    }

    .game-over-text {
        font-size: 4rem;
        font-weight: 900;
        color: var(--neon-pink);
        text-shadow: 0 0 20px var(--neon-pink);
        margin-bottom: 20px;
        transform: scale(0.8);
        transition: transform 2s ease-out;
    }
    
    .game-over-overlay.visible .game-over-text {
        transform: scale(1);
    }

    .final-score {
        font-size: 1.5rem;
        color: #fff;
    }

    /* Screen Shake Animation */
    @keyframes shake {
        0% { transform: translate(1px, 1px) rotate(0deg); }
        10% { transform: translate(-1px, -2px) rotate(-1deg); }
        20% { transform: translate(-3px, 0px) rotate(1deg); }
        30% { transform: translate(3px, 2px) rotate(0deg); }
        40% { transform: translate(1px, -1px) rotate(1deg); }
        50% { transform: translate(-1px, 2px) rotate(-1deg); }
        60% { transform: translate(-3px, 1px) rotate(0deg); }
        70% { transform: translate(3px, 1px) rotate(-1deg); }
        80% { transform: translate(-1px, -1px) rotate(1deg); }
        90% { transform: translate(1px, 2px) rotate(0deg); }
        100% { transform: translate(1px, -2px) rotate(-1deg); }
    }

    .shake {
        animation: shake 0.3s cubic-bezier(.36,.07,.19,.97) both;
    }

    /* Instructions / Decoration */
    .controls-hint {
        position: absolute;
        bottom: 20px;
        left: 0;
        width: 100%;
        text-align: center;
        font-size: 0.8rem;
        color: #444;
        pointer-events: none;
    }

</style>
</head> <body>
<div id="game-container">
    <canvas id="pinballCanvas"></canvas>
    
    <div class="ui-layer">
        <div class="score-display" id="score">0</div>
        <div class="ball-display" id="balls">Ball 1 / 3</div>
    </div>

    <div class="game-over-overlay" id="gameOver">
        <div class="game-over-text">GAME OVER</div>
        <div class="final-score" id="finalScore">Score: 0</div>
        <br>
        <div style="color: #666; font-size: 0.9rem;">Reload to play again</div>
    </div>
    
    <div class="controls-hint">AUTO-PILOT ENABLED</div>
</div>

<script>
    /**
     * Configuration & Setup
     */
    const canvas = document.getElementById('pinballCanvas');
    const ctx = canvas.getContext('2d');
    const container = document.getElementById('game-container');
    const scoreEl = document.getElementById('score');
    const ballsEl = document.getElementById('balls');
    const gameOverEl = document.getElementById('gameOver');
    const finalScoreEl = document.getElementById('finalScore');

    // Physics Constants
    const GRAVITY = 0.25; // How heavy the ball feels
    const FRICTION = 0.99; // Air resistance
    const WALL_BOUNCE = 0.6; // Energy loss on walls
    const BUMPER_BOUNCE = 1.3; // Energy gain on bumpers
    const FLIPPER_FORCE = 0.45; // Rotational speed force
    const SUBSTEPS = 8; // Physics steps per frame for stability

    // Game State
    let width, height;
    let score = 0;
    let balls = 3;
    let currentBall = 1;
    let gameActive = true;
    let shakeTimer = 0;

    /**
     * Resize Handler
     * Sets internal resolution to match logical CSS pixels for sharpness
     */
    function resize() {
        const rect = container.getBoundingClientRect();
        canvas.width = rect.width;
        canvas.height = rect.height;
        width = canvas.width;
        height = canvas.height;
        initTable();
    }
    window.addEventListener('resize', resize);

    /**
     * Vector Math Helper
     */
    const Vec2 = {
        add: (v1, v2) => ({ x: v1.x + v2.x, y: v1.y + v2.y }),
        sub: (v1, v2) => ({ x: v1.x - v2.x, y: v1.y - v2.y }),
        mult: (v, s) => ({ x: v.x * s, y: v.y * s }),
        dot: (v1, v2) => v1.x * v2.x + v1.y * v2.y,
        mag: (v) => Math.sqrt(v.x * v.x + v.y * v.y),
        norm: (v) => {
            const m = Math.sqrt(v.x * v.x + v.y * v.y);
            return m === 0 ? { x: 0, y: 0 } : { x: v.x / m, y: v.y / m };
        },
        dist: (v1, v2) => Math.sqrt((v1.x - v2.x) ** 2 + (v1.y - v2.y) ** 2),
        rotate: (v, angle) => ({
            x: v.x * Math.cos(angle) - v.y * Math.sin(angle),
            y: v.x * Math.sin(angle) + v.y * Math.cos(angle)
        })
    };

    /**
     * Classes
     */

    class Ball {
        constructor(x, y) {
            this.pos = { x, y };
            this.vel = { x: 0, y: 0 };
            this.radius = 12; // Base size
            this.mass = 1;
        }

        update() {
            // Apply Gravity
            this.vel.y += GRAVITY;
            this.vel = Vec2.mult(this.vel, FRICTION);

            // Update Position
            this.pos = Vec2.add(this.pos, this.vel);
        }

        draw() {
            // Shadow
            ctx.beginPath();
            ctx.arc(this.pos.x + 4, this.pos.y + 4, this.radius, 0, Math.PI * 2);
            ctx.fillStyle = 'rgba(0,0,0,0.3)';
            ctx.fill();

            // Chrome Effect
            ctx.beginPath();
            ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2);
            
            // Radial gradient for metallic sphere look
            const grad = ctx.createRadialGradient(
                this.pos.x - this.radius * 0.3, 
                this.pos.y - this.radius * 0.3, 
                this.radius * 0.1, 
                this.pos.x, 
                this.pos.y, 
                this.radius
            );
            grad.addColorStop(0, '#fff');        // Highlight
            grad.addColorStop(0.2, '#eee');      // Light Metal
            grad.addColorStop(0.5, '#889');      // Mid Metal
            grad.addColorStop(1, '#222');        // Shadow Edge
            
            ctx.fillStyle = grad;
            ctx.fill();
            
            // Specular highlight stroke
            ctx.strokeStyle = 'rgba(255,255,255,0.2)';
            ctx.lineWidth = 1;
            ctx.stroke();
        }
    }

    class Bumper {
        constructor(x, y, radius, scoreVal, color) {
            this.pos = { x, y };
            this.radius = radius;
            this.scoreVal = scoreVal;
            this.color = color;
            this.flashTime = 0;
        }

        draw() {
            const isFlashing = this.flashTime > 0;
            if (isFlashing) this.flashTime--;

            ctx.beginPath();
            ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.PI * 2);
            
            // Glow effect
            if (isFlashing) {
                ctx.shadowColor = this.color;
                ctx.shadowBlur = 30;
                ctx.fillStyle = '#fff'; // Flash white center
            } else {
                ctx.shadowColor = this.color;
                ctx.shadowBlur = 10;
                ctx.fillStyle = this.color; // Normal color
            }
            
            ctx.fill();
            ctx.shadowBlur = 0; // Reset
            
            // Rim
            ctx.strokeStyle = '#fff';
            ctx.lineWidth = 2;
            ctx.stroke();

            // Detail
            ctx.beginPath();
            ctx.arc(this.pos.x, this.pos.y, this.radius * 0.6, 0, Math.PI * 2);
            ctx.fillStyle = 'rgba(0,0,0,0.3)';
            ctx.fill();
        }
    }

    class Flipper {
        constructor(x, y, type, length, restAngle, activeAngle) {
            this.pivot = { x, y };
            this.type = type; // 'left' or 'right'
            this.length = length;
            this.restAngle = restAngle;
            this.activeAngle = activeAngle;
            this.currentAngle = restAngle;
            this.active = false;
            this.angularVelocity = 0;
            this.width = 15;
        }

        update() {
            const target = this.active ? this.activeAngle : this.restAngle;
            const diff = target - this.currentAngle;
            
            // Simple spring-like movement for angle
            if (this.active) {
                this.angularVelocity += diff * 0.2;
            } else {
                this.angularVelocity += diff * 0.1;
            }
            
            this.angularVelocity *= 0.6; // Damping
            this.currentAngle += this.angularVelocity;
        }

        getTip() {
            return {
                x: this.pivot.x + Math.cos(this.currentAngle) * this.length,
                y: this.pivot.y + Math.sin(this.currentAngle) * this.length
            };
        }

        draw() {
            const tip = this.getTip();
            
            ctx.save();
            ctx.lineCap = 'round';
            ctx.lineWidth = this.width;
            ctx.strokeStyle = this.active ? '#fff' : '#ddd';
            
            // Shadow
            ctx.shadowColor = 'rgba(0,0,0,0.5)';
            ctx.shadowBlur = 5;
            ctx.shadowOffsetX = 5;
            ctx.shadowOffsetY = 5;

            ctx.beginPath();
            ctx.moveTo(this.pivot.x, this.pivot.y);
            ctx.lineTo(tip.x, tip.y);
            ctx.stroke();

            // Pivot circle
            ctx.fillStyle = '#444';
            ctx.beginPath();
            ctx.arc(this.pivot.x, this.pivot.y, this.width/2, 0, Math.PI*2);
            ctx.fill();
            
            ctx.restore();
        }
    }

    /**
     * Game Objects
     */
    let ball;
    let bumpers = [];
    let flippers = [];
    let walls = []; // Array of line segments {p1, p2}

    function initTable() {
        if (!width || !height) return;

        // Reset arrays
        bumpers = [];
        flippers = [];
        walls = [];

        // Define Table Geometry
        // 1. Walls (Simple Box with angled bottom)
        const margin = 20;
        const drainGap = 100;

        // Outer Frame path points
        const tl = { x: margin, y: margin };
        const tr = { x: width - margin, y: margin };
        const bl = { x: margin, y: height - 120 };
        const br = { x: width - margin, y: height - 120 };
        
        // Lane entrances
        const bl_in = { x: width/2 - drainGap/2 - 40, y: height - 120 };
        const br_in = { x: width/2 + drainGap/2 + 40, y: height - 120 };
        
        // Flipper pivots
        const f_pivot_y = height - 100;
        const f_pivot_left = { x: width/2 - drainGap/2 - 10, y: f_pivot_y };
        const f_pivot_right = { x: width/2 + drainGap/2 + 10, y: f_pivot_y };

        // Create Walls segments
        walls.push({ p1: {x: margin, y: height}, p2: tl }); // Left
        walls.push({ p1: tl, p2: tr }); // Top
        walls.push({ p1: tr, p2: {x: width-margin, y: height} }); // Right
        
        // Slanted walls to flippers (The "In-lanes")
        walls.push({ p1: {x: margin, y: height*0.7}, p2: f_pivot_left }); 
        walls.push({ p1: {x: width-margin, y: height*0.7}, p2: f_pivot_right });

        // Top Arch (Approximated with segments for simplicity in collision)
        // Creating a "Corner" cut at top
        walls.push({ p1: tl, p2: {x: margin + 100, y: margin + 100} }); // Top Left Corner
        walls.push({ p1: tr, p2: {x: width - margin - 100, y: margin + 100} }); // Top Right Corner

        // Add Bumpers
        bumpers.push(new Bumper(width * 0.5, height * 0.3, 25, 100, '#0ff')); // Top Center
        bumpers.push(new Bumper(width * 0.3, height * 0.4, 20, 50, '#f0f')); // Left
        bumpers.push(new Bumper(width * 0.7, height * 0.4, 20, 50, '#f0f')); // Right
        bumpers.push(new Bumper(width * 0.5, height * 0.55, 15, 250, '#f80')); // Center Small

        // Add Flippers
        const flipperLen = 80;
        // Left Flipper: rests at ~30 deg, active at ~-30 deg
        flippers.push(new Flipper(f_pivot_left.x, f_pivot_left.y, 'left', flipperLen, Math.PI * 0.15, -Math.PI * 0.2));
        // Right Flipper: rests at ~150 deg, active at ~210 deg
        flippers.push(new Flipper(f_pivot_right.x, f_pivot_right.y, 'right', flipperLen, Math.PI * 0.85, Math.PI * 1.2));

        // Reset Ball
        resetBall();
    }

    function resetBall() {
        if (balls <= 0) {
            endGame();
            return;
        }
        // Launch from plunger lane (simulated)
        ball = new Ball(width - 40, height - 100);
        // Shoot up and left
        ball.vel = { x: -5 - Math.random() * 5, y: -20 - Math.random() * 5 };
        
        // Update UI
        ballsEl.innerText = `Ball ${currentBall} / 3`;
    }

    function endGame() {
        gameActive = false;
        finalScoreEl.innerText = `Score: ${Math.floor(score)}`;
        gameOverEl.classList.add('visible');
    }

    function triggerShake() {
        container.classList.remove('shake');
        void container.offsetWidth; // trigger reflow
        container.classList.add('shake');
    }

    /**
     * Physics Logic
     */
    
    // Point to Line Segment Distance/Collision
    function checkLineCollision(ball, p1, p2) {
        const vLine = Vec2.sub(p2, p1);
        const vBall = Vec2.sub(ball.pos, p1);
        const lineLen = Vec2.mag(vLine);
        const lineUnit = Vec2.norm(vLine);
        
        // Project ball onto line
        const proj = Vec2.dot(vBall, lineUnit);
        
        // Closest point on segment
        let closest;
        if (proj < 0) closest = p1;
        else if (proj > lineLen) closest = p2;
        else closest = Vec2.add(p1, Vec2.mult(lineUnit, proj));
        
        const distVec = Vec2.sub(ball.pos, closest);
        const dist = Vec2.mag(distVec);
        
        if (dist < ball.radius) {
            // Collision!
            const normal = Vec2.norm(distVec);
            
            // Push out
            const overlap = ball.radius - dist;
            ball.pos = Vec2.add(ball.pos, Vec2.mult(normal, overlap));
            
            // Reflect velocity
            const dot = Vec2.dot(ball.vel, normal);
            
            // Only bounce if moving into the wall
            if (dot < 0) {
                const reflect = Vec2.sub(ball.vel, Vec2.mult(normal, 2 * dot));
                ball.vel = Vec2.mult(reflect, WALL_BOUNCE);
                
                // Add a tiny bit of random noise to prevent infinite loops
                ball.vel.x += (Math.random() - 0.5);
            }
            return true;
        }
        return false;
    }

    // Ball to Circle (Bumper) Collision
    function checkCircleCollision(ball, circle) {
        const dist = Vec2.dist(ball.pos, circle.pos);
        const minDist = ball.radius + circle.radius;
        
        if (dist < minDist) {
            const normal = Vec2.norm(Vec2.sub(ball.pos, circle.pos));
            
            // Push out
            ball.pos = Vec2.add(circle.pos, Vec2.mult(normal, minDist));
            
            // Reflect
            const dot = Vec2.dot(ball.vel, normal);
            if (dot < 0) {
                const reflect = Vec2.sub(ball.vel, Vec2.mult(normal, 2 * dot));
                ball.vel = Vec2.mult(reflect, BUMPER_BOUNCE);
            }
            
            // Gameplay effects
            score += circle.scoreVal;
            scoreEl.innerText = Math.floor(score);
            circle.flashTime = 5; // Frames to flash
            
            // Shake if hit hard
            if (Vec2.mag(ball.vel) > 10) triggerShake();
            
            return true;
        }
        return false;
    }

    // Ball to Flipper Collision
    function checkFlipperCollision(ball, flipper) {
        // Treat flipper as a thick line segment
        const tip = flipper.getTip();
        const p1 = flipper.pivot;
        const p2 = tip;
        
        // Standard line check first
        const vLine = Vec2.sub(p2, p1);
        const vBall = Vec2.sub(ball.pos, p1);
        const lineLen = Vec2.mag(vLine);
        const lineUnit = Vec2.norm(vLine);
        const proj = Vec2.dot(vBall, lineUnit);
        
        let closest;
        if (proj < 0) closest = p1;
        else if (proj > lineLen) closest = p2;
        else closest = Vec2.add(p1, Vec2.mult(lineUnit, proj));
        
        const distVec = Vec2.sub(ball.pos, closest);
        const dist = Vec2.mag(distVec);
        
        const thickness = flipper.width / 2;
        const minDist = ball.radius + thickness;
        
        if (dist < minDist) {
            const normal = Vec2.norm(distVec);
            
            // Push out
            ball.pos = Vec2.add(closest, Vec2.mult(normal, minDist));
            
            // Velocity Reflection
            const dot = Vec2.dot(ball.vel, normal);
            if (dot < 0) {
                let reflect = Vec2.sub(ball.vel, Vec2.mult(normal, 2 * dot));
                ball.vel = Vec2.mult(reflect, 0.4); // Less bounce on flipper surface normally
                
                // Add Flipper Motion Energy
                // If flipper is moving towards ball, add huge boost
                if (flipper.active) {
                    // Calculate tangent velocity of flipper at impact point
                    const distFromPivot = Vec2.dist(closest, flipper.pivot);
                    const tipSpeed = 20 * FLIPPER_FORCE * (distFromPivot / flipper.length); 
                    
                    // Normal of flipper face
                    // Depending on Left or Right flipper, the normal points up/in
                    // Simplified: add velocity in the direction of the normal
                    ball.vel = Vec2.add(ball.vel, Vec2.mult(normal, tipSpeed));
                    triggerShake();
                }
            }
            return true;
        }
        return false;
    }

    /**
     * AI Controller
     */
    function updateAI() {
        if (!ball) return;
        
        // Reset flippers
        flippers.forEach(f => f.active = false);
        
        // Only care if ball is in lower half and moving down
        if (ball.pos.y > height * 0.6 && ball.vel.y > -2) {
            
            // Left Flipper Logic
            if (ball.pos.x < width * 0.5) {
                const f = flippers[0];
                // Prediction: will it cross the flipper line?
                // Simple distance check for now
                if (Vec2.dist(ball.pos, f.pivot) < f.length + 30) {
                    f.active = true;
                }
            } 
            // Right Flipper Logic
            else {
                const f = flippers[1];
                if (Vec2.dist(ball.pos, f.pivot) < f.length + 30) {
                    f.active = true;
                }
            }
        }
    }

    /**
     * Main Loop
     */
    function loop() {
        if (!gameActive) return;

        // Clear Canvas
        ctx.clearRect(0, 0, width, height);

        // Draw Background Elements (Lanes, Deco)
        ctx.strokeStyle = '#333';
        ctx.lineWidth = 4;
        // Draw lane lines
        ctx.beginPath();
        ctx.moveTo(width*0.1, 0); ctx.lineTo(width*0.1, height);
        ctx.moveTo(width*0.9, 0); ctx.lineTo(width*0.9, height);
        ctx.stroke();

        // Run Physics Substeps
        const dt = 1; 
        for (let i = 0; i < SUBSTEPS; i++) {
            if(ball) {
                ball.update();
                
                // Floor (Drain) Check
                if (ball.pos.y > height + 50) {
                    balls--;
                    if (balls > 0) {
                        currentBall++;
                        resetBall();
                    } else {
                        ball = null;
                        endGame();
                    }
                    break;
                }

                // Wall Collisions
                walls.forEach(w => checkLineCollision(ball, w.p1, w.p2));
                
                // Bumper Collisions
                bumpers.forEach(b => checkCircleCollision(ball, b));
                
                // Flipper Collisions
                flippers.forEach(f => checkFlipperCollision(ball, f));
            }
        }
        
        // Update AI
        updateAI();

        // Update Flippers (Visuals/Physics state)
        flippers.forEach(f => f.update());

        // Draw Entities
        // Bumpers
        bumpers.forEach(b => b.draw());

        // Walls (Visual)
        ctx.strokeStyle = '#0ff';
        ctx.lineWidth = 3;
        ctx.shadowColor = '#0ff';
        ctx.shadowBlur = 5;
        walls.forEach(w => {
            ctx.beginPath();
            ctx.moveTo(w.p1.x, w.p1.y);
            ctx.lineTo(w.p2.x, w.p2.y);
            ctx.stroke();
        });
        ctx.shadowBlur = 0;

        // Flippers
        flippers.forEach(f => f.draw());

        // Ball
        if (ball) ball.draw();

        requestAnimationFrame(loop);
    }

    // Init
    resize(); // Sets up canvas and table
    
    // Start Loop
    requestAnimationFrame(loop);

</script>
</body> </html>
Updated files
v1