Name of code : jeu du labyrinthe

Plateforme : www.codepen.com

Categorie : Jeux HTML

Mini jeu en JavaScript et css

Demo : jeu du labyrinthe

Javascript

HTML

CSS

Visual Sstudio Code

<!DOCTYPE html>
<html>
<head>
    <title>Title</title>
    <link rel="icon" type="image/png" href="LOGO" />
    <style>
        body {
        /* https://coolors.co/f06449-ede6e3-7d82b8-36382e-613f75  */
        --background-color: #ede6e3;
        --wall-color: #36382e;
        --joystick-color: #210124;
        --joystick-head-color: #f06449;
        --ball-color: #f06449;
        --end-color: #7d82b8;
        --text-color: #210124;

        font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
        background-color: var(--background-color);
        }

        html,
        body {
        height: 100%;
        margin: 0;
        }

        #center {
        display: flex;
        align-items: center;
        justify-content: center;
        height: 100%;
        }

        #game {
        display: grid;
        grid-template-columns: auto 150px;
        grid-template-rows: 1fr auto 1fr;
        gap: 30px;
        perspective: 600px;
        }

        #maze {
        position: relative;
        grid-row: 1 / -1;
        grid-column: 1;
        width: 350px;
        height: 315px;
        display: flex;
        justify-content: center;
        align-items: center;
        }

        #end {
        width: 65px;
        height: 65px;
        border: 5px dashed var(--end-color);
        border-radius: 50%;
        }

        #joystick {
        position: relative;
        background-color: var(--joystick-color);
        border-radius: 50%;
        width: 50px;
        height: 50px;
        display: flex;
        align-items: center;
        justify-content: center;
        margin: 10px 50px;
        grid-row: 2;
        }

        #joystick-head {
        position: relative;
        background-color: var(--joystick-head-color);
        border-radius: 50%;
        width: 20px;
        height: 20px;
        cursor: grab;

        animation-name: glow;
        animation-duration: 0.6s;
        animation-iteration-count: infinite;
        animation-direction: alternate;
        animation-timing-function: ease-in-out;
        animation-delay: 4s;
        }

        @keyframes glow {
        0% {
            transform: scale(1);
        }
        100% {
            transform: scale(1.2);
        }
        }

        .joystick-arrow:nth-of-type(1) {
        position: absolute;
        bottom: 55px;

        width: 0;
        height: 0;
        border-left: 10px solid transparent;
        border-right: 10px solid transparent;

        border-bottom: 10px solid var(--joystick-color);
        }

        .joystick-arrow:nth-of-type(2) {
        position: absolute;
        top: 55px;

        width: 0;
        height: 0;
        border-left: 10px solid transparent;
        border-right: 10px solid transparent;

        border-top: 10px solid var(--joystick-color);
        }

        .joystick-arrow:nth-of-type(3) {
        position: absolute;
        left: 55px;

        width: 0;
        height: 0;
        border-top: 10px solid transparent;
        border-bottom: 10px solid transparent;

        border-left: 10px solid var(--joystick-color);
        }

        .joystick-arrow:nth-of-type(4) {
        position: absolute;
        right: 55px;

        width: 0;
        height: 0;
        border-top: 10px solid transparent;
        border-bottom: 10px solid transparent;

        border-right: 10px solid var(--joystick-color);
        }

        #note {
        grid-row: 3;
        grid-column: 2;
        text-align: center;
        font-size: 0.8em;
        color: var(--text-color);
        transition: opacity 2s;
        }

        a:visited {
        color: inherit;
        }

        .ball {
        position: absolute;
        margin-top: -5px;
        margin-left: -5px;
        border-radius: 50%;
        background-color: var(--ball-color);
        width: 10px;
        height: 10px;
        }

        .wall {
        position: absolute;
        background-color: var(--wall-color);
        transform-origin: top center;
        margin-left: -5px;
        }

        .wall::before,
        .wall::after {
        display: block;
        content: "";
        width: 10px;
        height: 10px;
        background-color: inherit;
        border-radius: 50%;
        position: absolute;
        }

        .wall::before {
        top: -5px;
        }

        .wall::after {
        bottom: -5px;
        }

        .black-hole {
        position: absolute;
        margin-top: -9px;
        margin-left: -9px;
        border-radius: 50%;
        background-color: black;
        width: 18px;
        height: 18px;
        }

        }

    </style>
</head>
<body>
    <div id="center">
        <div id="game">
          <div id="maze">
            <div id="end"></div>
          </div>
          <div id="joystick">
            <div class="joystick-arrow"></div>
            <div class="joystick-arrow"></div>
            <div class="joystick-arrow"></div>
            <div class="joystick-arrow"></div>
            <div id="joystick-head"></div>
          </div>
          <div id="note">
            Click the joystick to start!
            <p>Move every ball to the center. Ready for hard mode? Press H</p>
          </div>
        </div>
      </div>
     

</body>
<script>

    Math.minmax = (value, limit) => {
    return Math.max(Math.min(value, limit), -limit);
    };

    const distance2D = (p1, p2) => {
    return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2);
    };

    // Angle between the two points
    const getAngle = (p1, p2) => {
    let angle = Math.atan((p2.y - p1.y) / (p2.x - p1.x));
    if (p2.x - p1.x < 0) angle += Math.PI;
    return angle;
    };

    // The closest a ball and a wall cap can be
    const closestItCanBe = (cap, ball) => {
    let angle = getAngle(cap, ball);

    const deltaX = Math.cos(angle) * (wallW / 2 + ballSize / 2);
    const deltaY = Math.sin(angle) * (wallW / 2 + ballSize / 2);

    return { x: cap.x + deltaX, y: cap.y + deltaY };
    };

    // Roll the ball around the wall cap
    const rollAroundCap = (cap, ball) => {
    // The direction the ball can't move any further because the wall holds it back
    let impactAngle = getAngle(ball, cap);

    // The direction the ball wants to move based on it's velocity
    let heading = getAngle(
        { x: 0, y: 0 },
        { x: ball.velocityX, y: ball.velocityY }
    );

    // The angle between the impact direction and the ball's desired direction
    // The smaller this angle is, the bigger the impact
    // The closer it is to 90 degrees the smoother it gets (at 90 there would be no collision)
    let impactHeadingAngle = impactAngle - heading;

    // Velocity distance if not hit would have occurred
    const velocityMagnitude = distance2D(
        { x: 0, y: 0 },
        { x: ball.velocityX, y: ball.velocityY }
    );
    // Velocity component diagonal to the impact
    const velocityMagnitudeDiagonalToTheImpact =
        Math.sin(impactHeadingAngle) * velocityMagnitude;

    // How far should the ball be from the wall cap
    const closestDistance = wallW / 2 + ballSize / 2;

    const rotationAngle = Math.atan(
        velocityMagnitudeDiagonalToTheImpact / closestDistance
    );

    const deltaFromCap = {
        x: Math.cos(impactAngle + Math.PI - rotationAngle) * closestDistance,
        y: Math.sin(impactAngle + Math.PI - rotationAngle) * closestDistance
    };

    const x = ball.x;
    const y = ball.y;
    const velocityX = ball.x - (cap.x + deltaFromCap.x);
    const velocityY = ball.y - (cap.y + deltaFromCap.y);
    const nextX = x + velocityX;
    const nextY = y + velocityY;

    return { x, y, velocityX, velocityY, nextX, nextY };
    };

    // Decreases the absolute value of a number but keeps it's sign, doesn't go below abs 0
    const slow = (number, difference) => {
    if (Math.abs(number) <= difference) return 0;
    if (number > difference) return number - difference;
    return number + difference;
    };

    const mazeElement = document.getElementById("maze");
    const joystickHeadElement = document.getElementById("joystick-head");
    const noteElement = document.getElementById("note"); // Note element for instructions and game won, game failed texts

    let hardMode = false;
    let previousTimestamp;
    let gameInProgress;
    let mouseStartX;
    let mouseStartY;
    let accelerationX;
    let accelerationY;
    let frictionX;
    let frictionY;

    const pathW = 25; // Path width
    const wallW = 10; // Wall width
    const ballSize = 10; // Width and height of the ball
    const holeSize = 18;

    const debugMode = false;

    let balls = [];
    let ballElements = [];
    let holeElements = [];

    resetGame();

    // Draw balls for the first time
    balls.forEach(({ x, y }) => {
    const ball = document.createElement("div");
    ball.setAttribute("class", "ball");
    ball.style.cssText = `left: ${x}px; top: ${y}px; `;

    mazeElement.appendChild(ball);
    ballElements.push(ball);
    });

    // Wall metadata
    const walls = [
    // Border
    { column: 0, row: 0, horizontal: true, length: 10 },
    { column: 0, row: 0, horizontal: false, length: 9 },
    { column: 0, row: 9, horizontal: true, length: 10 },
    { column: 10, row: 0, horizontal: false, length: 9 },

    // Horizontal lines starting in 1st column
    { column: 0, row: 6, horizontal: true, length: 1 },
    { column: 0, row: 8, horizontal: true, length: 1 },

    // Horizontal lines starting in 2nd column
    { column: 1, row: 1, horizontal: true, length: 2 },
    { column: 1, row: 7, horizontal: true, length: 1 },

    // Horizontal lines starting in 3rd column
    { column: 2, row: 2, horizontal: true, length: 2 },
    { column: 2, row: 4, horizontal: true, length: 1 },
    { column: 2, row: 5, horizontal: true, length: 1 },
    { column: 2, row: 6, horizontal: true, length: 1 },

    // Horizontal lines starting in 4th column
    { column: 3, row: 3, horizontal: true, length: 1 },
    { column: 3, row: 8, horizontal: true, length: 3 },

    // Horizontal lines starting in 5th column
    { column: 4, row: 6, horizontal: true, length: 1 },

    // Horizontal lines starting in 6th column
    { column: 5, row: 2, horizontal: true, length: 2 },
    { column: 5, row: 7, horizontal: true, length: 1 },

    // Horizontal lines starting in 7th column
    { column: 6, row: 1, horizontal: true, length: 1 },
    { column: 6, row: 6, horizontal: true, length: 2 },

    // Horizontal lines starting in 8th column
    { column: 7, row: 3, horizontal: true, length: 2 },
    { column: 7, row: 7, horizontal: true, length: 2 },

    // Horizontal lines starting in 9th column
    { column: 8, row: 1, horizontal: true, length: 1 },
    { column: 8, row: 2, horizontal: true, length: 1 },
    { column: 8, row: 3, horizontal: true, length: 1 },
    { column: 8, row: 4, horizontal: true, length: 2 },
    { column: 8, row: 8, horizontal: true, length: 2 },

    // Vertical lines after the 1st column
    { column: 1, row: 1, horizontal: false, length: 2 },
    { column: 1, row: 4, horizontal: false, length: 2 },

    // Vertical lines after the 2nd column
    { column: 2, row: 2, horizontal: false, length: 2 },
    { column: 2, row: 5, horizontal: false, length: 1 },
    { column: 2, row: 7, horizontal: false, length: 2 },

    // Vertical lines after the 3rd column
    { column: 3, row: 0, horizontal: false, length: 1 },
    { column: 3, row: 4, horizontal: false, length: 1 },
    { column: 3, row: 6, horizontal: false, length: 2 },

    // Vertical lines after the 4th column
    { column: 4, row: 1, horizontal: false, length: 2 },
    { column: 4, row: 6, horizontal: false, length: 1 },

    // Vertical lines after the 5th column
    { column: 5, row: 0, horizontal: false, length: 2 },
    { column: 5, row: 6, horizontal: false, length: 1 },
    { column: 5, row: 8, horizontal: false, length: 1 },

    // Vertical lines after the 6th column
    { column: 6, row: 4, horizontal: false, length: 1 },
    { column: 6, row: 6, horizontal: false, length: 1 },

    // Vertical lines after the 7th column
    { column: 7, row: 1, horizontal: false, length: 4 },
    { column: 7, row: 7, horizontal: false, length: 2 },

    // Vertical lines after the 8th column
    { column: 8, row: 2, horizontal: false, length: 1 },
    { column: 8, row: 4, horizontal: false, length: 2 },

    // Vertical lines after the 9th column
    { column: 9, row: 1, horizontal: false, length: 1 },
    { column: 9, row: 5, horizontal: false, length: 2 }
    ].map((wall) => ({
    x: wall.column * (pathW + wallW),
    y: wall.row * (pathW + wallW),
    horizontal: wall.horizontal,
    length: wall.length * (pathW + wallW)
    }));

    // Draw walls
    walls.forEach(({ x, y, horizontal, length }) => {
    const wall = document.createElement("div");
    wall.setAttribute("class", "wall");
    wall.style.cssText = `
        left: ${x}px;
        top: ${y}px;
        width: ${wallW}px;
        height: ${length}px;
        transform: rotate(${horizontal ? -90 : 0}deg);
        `;

    mazeElement.appendChild(wall);
    });

    const holes = [
    { column: 0, row: 5 },
    { column: 2, row: 0 },
    { column: 2, row: 4 },
    { column: 4, row: 6 },
    { column: 6, row: 2 },
    { column: 6, row: 8 },
    { column: 8, row: 1 },
    { column: 8, row: 2 }
    ].map((hole) => ({
    x: hole.column * (wallW + pathW) + (wallW / 2 + pathW / 2),
    y: hole.row * (wallW + pathW) + (wallW / 2 + pathW / 2)
    }));

    joystickHeadElement.addEventListener("mousedown", function (event) {
    if (!gameInProgress) {
        mouseStartX = event.clientX;
        mouseStartY = event.clientY;
        gameInProgress = true;
        window.requestAnimationFrame(main);
        noteElement.style.opacity = 0;
        joystickHeadElement.style.cssText = `
            animation: none;
            cursor: grabbing;
        `;
    }
    });

    window.addEventListener("mousemove", function (event) {
    if (gameInProgress) {
        const mouseDeltaX = -Math.minmax(mouseStartX - event.clientX, 15);
        const mouseDeltaY = -Math.minmax(mouseStartY - event.clientY, 15);

        joystickHeadElement.style.cssText = `
            left: ${mouseDeltaX}px;
            top: ${mouseDeltaY}px;
            animation: none;
            cursor: grabbing;
        `;

        const rotationY = mouseDeltaX * 0.8; // Max rotation = 12
        const rotationX = mouseDeltaY * 0.8;

        mazeElement.style.cssText = `
            transform: rotateY(${rotationY}deg) rotateX(${-rotationX}deg)
        `;

        const gravity = 2;
        const friction = 0.01; // Coefficients of friction

        accelerationX = gravity * Math.sin((rotationY / 180) * Math.PI);
        accelerationY = gravity * Math.sin((rotationX / 180) * Math.PI);
        frictionX = gravity * Math.cos((rotationY / 180) * Math.PI) * friction;
        frictionY = gravity * Math.cos((rotationX / 180) * Math.PI) * friction;
    }
    });

    window.addEventListener("keydown", function (event) {
    // If not an arrow key or space or H was pressed then return
    if (![" ", "H", "h", "E", "e"].includes(event.key)) return;

    // If an arrow key was pressed then first prevent default
    event.preventDefault();

    // If space was pressed restart the game
    if (event.key == " ") {
        resetGame();
        return;
    }

    // Set Hard mode
    if (event.key == "H" || event.key == "h") {
        hardMode = true;
        resetGame();
        return;
    }

    // Set Easy mode
    if (event.key == "E" || event.key == "e") {
        hardMode = false;
        resetGame();
        return;
    }
    });

    function resetGame() {
    previousTimestamp = undefined;
    gameInProgress = false;
    mouseStartX = undefined;
    mouseStartY = undefined;
    accelerationX = undefined;
    accelerationY = undefined;
    frictionX = undefined;
    frictionY = undefined;

    mazeElement.style.cssText = `
        transform: rotateY(0deg) rotateX(0deg)
        `;

    joystickHeadElement.style.cssText = `
        left: 0;
        top: 0;
        animation: glow;
        cursor: grab;
        `;

    if (hardMode) {
        noteElement.innerHTML = `Click the joystick to start!
            <p>Hard mode, Avoid black holes. Back to easy mode? Press E</p>`;
    } else {
        noteElement.innerHTML = `Click the joystick to start!
            <p>Move every ball to the center. Ready for hard mode? Press H</p>`;
    }
    noteElement.style.opacity = 1;

    balls = [
        { column: 0, row: 0 },
        { column: 9, row: 0 },
        { column: 0, row: 8 },
        { column: 9, row: 8 }
    ].map((ball) => ({
        x: ball.column * (wallW + pathW) + (wallW / 2 + pathW / 2),
        y: ball.row * (wallW + pathW) + (wallW / 2 + pathW / 2),
        velocityX: 0,
        velocityY: 0
    }));

    if (ballElements.length) {
        balls.forEach(({ x, y }, index) => {
        ballElements[index].style.cssText = `left: ${x}px; top: ${y}px; `;
        });
    }

    // Remove previous hole elements
    holeElements.forEach((holeElement) => {
        mazeElement.removeChild(holeElement);
    });
    holeElements = [];

    // Reset hole elements if hard mode
    if (hardMode) {
        holes.forEach(({ x, y }) => {
        const ball = document.createElement("div");
        ball.setAttribute("class", "black-hole");
        ball.style.cssText = `left: ${x}px; top: ${y}px; `;

        mazeElement.appendChild(ball);
        holeElements.push(ball);
        });
    }
    }

    function main(timestamp) {
    // It is possible to reset the game mid-game. This case the look should stop
    if (!gameInProgress) return;

    if (previousTimestamp === undefined) {
        previousTimestamp = timestamp;
        window.requestAnimationFrame(main);
        return;
    }

    const maxVelocity = 1.5;

    // Time passed since last cycle divided by 16
    // This function gets called every 16 ms on average so dividing by 16 will result in 1
    const timeElapsed = (timestamp - previousTimestamp) / 16;

    try {
        // If mouse didn't move yet don't do anything
        if (accelerationX != undefined && accelerationY != undefined) {
        const velocityChangeX = accelerationX * timeElapsed;
        const velocityChangeY = accelerationY * timeElapsed;
        const frictionDeltaX = frictionX * timeElapsed;
        const frictionDeltaY = frictionY * timeElapsed;

        balls.forEach((ball) => {
            if (velocityChangeX == 0) {
            // No rotation, the plane is flat
            // On flat surface friction can only slow down, but not reverse movement
            ball.velocityX = slow(ball.velocityX, frictionDeltaX);
            } else {
            ball.velocityX = ball.velocityX + velocityChangeX;
            ball.velocityX = Math.max(Math.min(ball.velocityX, 1.5), -1.5);
            ball.velocityX =
                ball.velocityX - Math.sign(velocityChangeX) * frictionDeltaX;
            ball.velocityX = Math.minmax(ball.velocityX, maxVelocity);
            }

            if (velocityChangeY == 0) {
            // No rotation, the plane is flat
            // On flat surface friction can only slow down, but not reverse movement
            ball.velocityY = slow(ball.velocityY, frictionDeltaY);
            } else {
            ball.velocityY = ball.velocityY + velocityChangeY;
            ball.velocityY =
                ball.velocityY - Math.sign(velocityChangeY) * frictionDeltaY;
            ball.velocityY = Math.minmax(ball.velocityY, maxVelocity);
            }

            // Preliminary next ball position, only becomes true if no hit occurs
            // Used only for hit testing, does not mean that the ball will reach this position
            ball.nextX = ball.x + ball.velocityX;
            ball.nextY = ball.y + ball.velocityY;

            if (debugMode) console.log("tick", ball);

            walls.forEach((wall, wi) => {
            if (wall.horizontal) {
                // Horizontal wall

                if (
                ball.nextY + ballSize / 2 >= wall.y - wallW / 2 &&
                ball.nextY - ballSize / 2 <= wall.y + wallW / 2
                ) {
                // Ball got within the strip of the wall
                // (not necessarily hit it, could be before or after)

                const wallStart = {
                    x: wall.x,
                    y: wall.y
                };
                const wallEnd = {
                    x: wall.x + wall.length,
                    y: wall.y
                };

                if (
                    ball.nextX + ballSize / 2 >= wallStart.x - wallW / 2 &&
                    ball.nextX < wallStart.x
                ) {
                    // Ball might hit the left cap of a horizontal wall
                    const distance = distance2D(wallStart, {
                    x: ball.nextX,
                    y: ball.nextY
                    });
                    if (distance < ballSize / 2 + wallW / 2) {
                    if (debugMode && wi > 4)
                        console.warn("too close h head", distance, ball);

                    // Ball hits the left cap of a horizontal wall
                    const closest = closestItCanBe(wallStart, {
                        x: ball.nextX,
                        y: ball.nextY
                    });
                    const rolled = rollAroundCap(wallStart, {
                        x: closest.x,
                        y: closest.y,
                        velocityX: ball.velocityX,
                        velocityY: ball.velocityY
                    });

                    Object.assign(ball, rolled);
                    }
                }

                if (
                    ball.nextX - ballSize / 2 <= wallEnd.x + wallW / 2 &&
                    ball.nextX > wallEnd.x
                ) {
                    // Ball might hit the right cap of a horizontal wall
                    const distance = distance2D(wallEnd, {
                    x: ball.nextX,
                    y: ball.nextY
                    });
                    if (distance < ballSize / 2 + wallW / 2) {
                    if (debugMode && wi > 4)
                        console.warn("too close h tail", distance, ball);

                    // Ball hits the right cap of a horizontal wall
                    const closest = closestItCanBe(wallEnd, {
                        x: ball.nextX,
                        y: ball.nextY
                    });
                    const rolled = rollAroundCap(wallEnd, {
                        x: closest.x,
                        y: closest.y,
                        velocityX: ball.velocityX,
                        velocityY: ball.velocityY
                    });

                    Object.assign(ball, rolled);
                    }
                }

                if (ball.nextX >= wallStart.x && ball.nextX <= wallEnd.x) {
                    // The ball got inside the main body of the wall
                    if (ball.nextY < wall.y) {
                    // Hit horizontal wall from top
                    ball.nextY = wall.y - wallW / 2 - ballSize / 2;
                    } else {
                    // Hit horizontal wall from bottom
                    ball.nextY = wall.y + wallW / 2 + ballSize / 2;
                    }
                    ball.y = ball.nextY;
                    ball.velocityY = -ball.velocityY / 3;

                    if (debugMode && wi > 4)
                    console.error("crossing h line, HIT", ball);
                }
                }
            } else {
                // Vertical wall

                if (
                ball.nextX + ballSize / 2 >= wall.x - wallW / 2 &&
                ball.nextX - ballSize / 2 <= wall.x + wallW / 2
                ) {
                // Ball got within the strip of the wall
                // (not necessarily hit it, could be before or after)

                const wallStart = {
                    x: wall.x,
                    y: wall.y
                };
                const wallEnd = {
                    x: wall.x,
                    y: wall.y + wall.length
                };

                if (
                    ball.nextY + ballSize / 2 >= wallStart.y - wallW / 2 &&
                    ball.nextY < wallStart.y
                ) {
                    // Ball might hit the top cap of a horizontal wall
                    const distance = distance2D(wallStart, {
                    x: ball.nextX,
                    y: ball.nextY
                    });
                    if (distance < ballSize / 2 + wallW / 2) {
                    if (debugMode && wi > 4)
                        console.warn("too close v head", distance, ball);

                    // Ball hits the left cap of a horizontal wall
                    const closest = closestItCanBe(wallStart, {
                        x: ball.nextX,
                        y: ball.nextY
                    });
                    const rolled = rollAroundCap(wallStart, {
                        x: closest.x,
                        y: closest.y,
                        velocityX: ball.velocityX,
                        velocityY: ball.velocityY
                    });

                    Object.assign(ball, rolled);
                    }
                }

                if (
                    ball.nextY - ballSize / 2 <= wallEnd.y + wallW / 2 &&
                    ball.nextY > wallEnd.y
                ) {
                    // Ball might hit the bottom cap of a horizontal wall
                    const distance = distance2D(wallEnd, {
                    x: ball.nextX,
                    y: ball.nextY
                    });
                    if (distance < ballSize / 2 + wallW / 2) {
                    if (debugMode && wi > 4)
                        console.warn("too close v tail", distance, ball);

                    // Ball hits the right cap of a horizontal wall
                    const closest = closestItCanBe(wallEnd, {
                        x: ball.nextX,
                        y: ball.nextY
                    });
                    const rolled = rollAroundCap(wallEnd, {
                        x: closest.x,
                        y: closest.y,
                        velocityX: ball.velocityX,
                        velocityY: ball.velocityY
                    });

                    Object.assign(ball, rolled);
                    }
                }

                if (ball.nextY >= wallStart.y && ball.nextY <= wallEnd.y) {
                    // The ball got inside the main body of the wall
                    if (ball.nextX < wall.x) {
                    // Hit vertical wall from left
                    ball.nextX = wall.x - wallW / 2 - ballSize / 2;
                    } else {
                    // Hit vertical wall from right
                    ball.nextX = wall.x + wallW / 2 + ballSize / 2;
                    }
                    ball.x = ball.nextX;
                    ball.velocityX = -ball.velocityX / 3;

                    if (debugMode && wi > 4)
                    console.error("crossing v line, HIT", ball);
                }
                }
            }
            });

            // Detect is a ball fell into a hole
            if (hardMode) {
            holes.forEach((hole, hi) => {
                const distance = distance2D(hole, {
                x: ball.nextX,
                y: ball.nextY
                });

                if (distance <= holeSize / 2) {
                // The ball fell into a hole
                holeElements[hi].style.backgroundColor = "red";
                throw Error("The ball fell into a hole");
                }
            });
            }

            // Adjust ball metadata
            ball.x = ball.x + ball.velocityX;
            ball.y = ball.y + ball.velocityY;
        });

        // Move balls to their new position on the UI
        balls.forEach(({ x, y }, index) => {
            ballElements[index].style.cssText = `left: ${x}px; top: ${y}px; `;
        });
        }

        // Win detection
        if (
        balls.every(
            (ball) => distance2D(ball, { x: 350 / 2, y: 315 / 2 }) < 65 / 2
        )
        ) {
        noteElement.innerHTML = `Congrats, you did it!
            ${!hardMode ? "<p>Press H for hard mode</p>" : ""}
            <p>
            Follow me
            <a href="https://twitter.com/HunorBorbely" , target="_blank"
                >@HunorBorbely</a
            >
            </p>`;
        noteElement.style.opacity = 1;
        gameInProgress = false;
        } else {
        previousTimestamp = timestamp;
        window.requestAnimationFrame(main);
        }
    } catch (error) {
        if (error.message == "The ball fell into a hole") {
        noteElement.innerHTML = `A ball fell into a black hole! Press space to reset the game.
            <p>
            Back to easy? Press E
            </p>`;
        noteElement.style.opacity = 1;
        gameInProgress = false;
        } else throw error;
    }
    }
</script>
</html>