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 a to add more balls to the screen, or b to lower the number of balls. Beware that both of these actions will cost you 2 points!

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.setStayInScreen(true); player.setScore(0); player.showPlayer = false; return output; } function createBall() { let ball = sprites.create(BALL_IMAGE.clone(), SpriteKind.Enemy); ball.vy = randint(-20, 20); ball.vx = 60 * (Math.percentChance(50) ? 1 : -1); } game.onUpdate(function () { sprites .allOfKind(SpriteKind.Enemy) .forEach(b => { 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); 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( randint(1, otherSprite.image.width - 2), randint(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 (!next) return; 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) => { 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; } } });