Ping!

Bounce a ball between two paddles at either side of the screen. You and another player can move the paddles to return a ball. However, paddles that aren’t controlled will move on their own. The auto-controlled paddles happen at the beginning of the game or shortly after a player input stops.

If you’re feeling courageous, you can press ||controller:a|| to add more balls to the screen, or ||controller:b|| to lower the number of balls. Beware that both of these actions will cost you 2 points!

const DESTROYED_KEY = "__destroyed";
const BALL_IMAGE = img`
    . . e e 1 e e e . .
    . e 1 1 d d d d e .
    e 1 d d d d d d d e
    e d d d d d d d d e
    e d d d d d d d d e
    e d d d d d d d d e
    e d d d d d d d d e
    . e d d d d d d e .
    . . e e e e e e . .
`;

const PADDLE_SPEED = 150;
const PADDING_FROM_WALL = 3;
let pingMessage = false;

// if player doesn't interact for 'TIMEOUT' time, revert to ai
const TIMEOUT = 5000;
let playerOneLastMove = -TIMEOUT;
let playerTwoLastMove = -TIMEOUT;

controller.setRepeatDefault(0, 1000);

controller.up.onEvent(ControllerButtonEvent.Repeated, () => playerOneLastMove = game.runtime());
controller.down.onEvent(ControllerButtonEvent.Repeated, () => playerOneLastMove = game.runtime());
controller.player2.up.onEvent(ControllerButtonEvent.Repeated, () => playerTwoLastMove = game.runtime());
controller.player2.down.onEvent(ControllerButtonEvent.Repeated, () => playerTwoLastMove = game.runtime());

const playerOne = createPlayer(info.player1);
playerOne.left = PADDING_FROM_WALL;
controller.moveSprite(playerOne, 0, PADDLE_SPEED);

const playerTwo = createPlayer(info.player2);
playerTwo.right = screen.width - PADDING_FROM_WALL;
controller.player2.moveSprite(playerTwo, 0, PADDLE_SPEED);

createBall();

function createPlayer(player: info.PlayerInfo) {
    const output = sprites.create(image.create(3, 18), SpriteKind.Player);

    output.image.fill(player.bg);
    output.setFlag(SpriteFlag.StayInScreen, true);

    player.setScore(0);
    player.showPlayer = false;

    return output;
}

function createBall() {
    let ball = sprites.create(BALL_IMAGE.clone(), SpriteKind.Enemy);
    ball.vy = Math.randomRange(-20, 20);
    ball.vx = 60 * (Math.percentChance(50) ? 1 : -1);
    ball.data = {};
}

game.onUpdate(function () {
    sprites
        .allOfKind(SpriteKind.Enemy)
        .forEach(b => {
            // tagged as destroyed
            if (b.data[DESTROYED_KEY]) return;

            const scoreRight = b.x < 0;
            const scoreLeft = b.x >= screen.width;

            if (scoreRight) {
                info.player2.changeScoreBy(1)
            } else if (scoreLeft) {
                info.player1.changeScoreBy(1)
            }

            if (b.top < 0) {
                b.vy = Math.abs(b.vy);
            } else if (b.bottom > screen.height) {
                b.vy = -Math.abs(b.vy);
            }

            if (scoreLeft || scoreRight) {
                b.destroy(effects.disintegrate, 500);
                b.data[DESTROYED_KEY] = true;
                control.runInParallel(function () {
                    pause(250);
                    createBall();
                });
            }
        }
        );
});

game.onShade(function () {
    if (pingMessage) {
        screen.printCenter("ping", 5);
    } else {
        screen.printCenter("pong", 5);
    }
})

sprites.onOverlap(SpriteKind.Player, SpriteKind.Enemy,
    (sprite: Sprite, otherSprite: Sprite) => {
        const fromCenter = otherSprite.y - sprite.y;

        otherSprite.vx = otherSprite.vx * -1.05;
        otherSprite.vy += (sprite.vy >> 1) + (fromCenter * 3);

        otherSprite.startEffect(effects.ashes, 150);
        sprite.startEffect(effects.ashes, 100);

        otherSprite.image.setPixel(
            Math.randomRange(1, otherSprite.image.width - 2),
            Math.randomRange(1, otherSprite.image.height - 2),
            sprite.image.getPixel(0, 0)
        );

        pingMessage = !pingMessage;

        // time out this event so it doesn't retrigger on the same collision
        pause(500);
    }
);

controller.A.onEvent(ControllerButtonEvent.Pressed, () => addBall(info.player1));
controller.B.onEvent(ControllerButtonEvent.Pressed, () => removeBall(info.player1));
controller.player2.A.onEvent(ControllerButtonEvent.Pressed, () => addBall(info.player2));
controller.player2.B.onEvent(ControllerButtonEvent.Pressed, () => removeBall(info.player2));

function addBall(player: info.PlayerInfo) {
    player.changeScoreBy(-2);
    createBall();
}

function removeBall(player: info.PlayerInfo) {
    const balls = sprites.allOfKind(SpriteKind.Enemy);
    if (balls.length > 1) {
        Math.pickRandom(balls).destroy();
        player.changeScoreBy(-2);
    }
}

game.onUpdate(function () {
    const currTime = game.runtime();

    if (playerOneLastMove + TIMEOUT < currTime) {
        trackBall(playerOne);
    }

    if (playerTwoLastMove + TIMEOUT < currTime) {
        trackBall(playerTwo);
    }

    function trackBall(player: Sprite) {
        const next = nextBall(player);

        if (ballFacingPlayer(player, next)) {
            // move to where ball is expected to intersect
            intersectBall(player, next);
        } else {
            // relax, ball is going other way
            player.vy = 0;
        }
    }

    function nextBall(player: Sprite) {
        return sprites
            .allOfKind(SpriteKind.Enemy)
            .sort((a, b) => {
                // if one of the balls is destroyed, ignore it
                if (a.data) return 1;
                else if (b.data) return -1;

                const aFacingPlayer = ballFacingPlayer(player, a);
                const bFacingPlayer = ballFacingPlayer(player, b);

                // else prefer ball facing player
                if (aFacingPlayer && !bFacingPlayer) return -1;
                else if (!aFacingPlayer && bFacingPlayer) return 1;

                // else prefer ball that will next reach player
                const aDiff = Math.abs((a.x - player.x) / a.vx);
                const bDiff = Math.abs((b.x - player.x) / b.vx);
                return aDiff - bDiff;
            })[0];
    }

    function ballFacingPlayer(player: Sprite, ball: Sprite) {
        return (ball.vx < 0 && player.x < 80) || (ball.vx > 0 && player.x > 80);
    }

    function intersectBall(player: Sprite, target: Sprite) {
        const projectedDY = (target.x - player.x) * target.vy / target.vx;
        let intersectionPoint = target.y - projectedDY;

        // quick 'estimation' for vertical bounces
        if (intersectionPoint < 0) {
            intersectionPoint = Math.abs(intersectionPoint % screen.height)
        } else if (intersectionPoint > screen.height) {
            intersectionPoint -= intersectionPoint % screen.height;
        }

        // move toward estimated intersection point if not in range
        if (intersectionPoint > player.y + (player.height >> 2)) {
            player.vy = PADDLE_SPEED;
        } else if (intersectionPoint < player.y - (player.height >> 2)) {
            player.vy = -PADDLE_SPEED;
        } else {
            player.vy = 0;
        }
    }
});