From 64c1804f6bde432ba67eaaaa797308f2660cd857 Mon Sep 17 00:00:00 2001 From: homecoc Date: Mon, 21 Sep 2026 14:33:34 -0400 Subject: [PATCH 1/2] Add standalone HTML, CSS, and JavaScript Pong game --- public/pong/game.js | 150 ++++++++++++++++++++++++++++++++++ public/pong/index.html | 43 ++++++++++ public/pong/styles.css | 177 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 370 insertions(+) create mode 100644 public/pong/game.js create mode 100644 public/pong/index.html create mode 100644 public/pong/styles.css diff --git a/public/pong/game.js b/public/pong/game.js new file mode 100644 index 000000000000..09d67c58a303 --- /dev/null +++ b/public/pong/game.js @@ -0,0 +1,150 @@ +const canvas = document.getElementById('game-canvas'); +const context = canvas.getContext('2d'); +const playerScoreElement = document.getElementById('player-score'); +const computerScoreElement = document.getElementById('computer-score'); +const messageElement = document.getElementById('game-message'); +const resetButton = document.getElementById('reset-button'); + +const court = { width: canvas.width, height: canvas.height }; +const paddle = { width: 16, height: 104, speed: 8 }; +const ball = { size: 14, speed: 6, maxSpeed: 11 }; + +const player = { x: 30, y: 0, score: 0, velocity: 0 }; +const computer = { x: court.width - 46, y: 0, score: 0, velocity: 0 }; +const keys = new Set(); +let animationFrame; +let gameStarted = false; + +function resetBall(direction = Math.random() > 0.5 ? 1 : -1) { + ball.x = court.width / 2 - ball.size / 2; + ball.y = court.height / 2 - ball.size / 2; + ball.velocityX = direction * ball.speed; + ball.velocityY = (Math.random() * 4 - 2) || 1; + gameStarted = false; + messageElement.textContent = 'Press an arrow key to serve'; + messageElement.classList.remove('hidden'); +} + +function resetGame() { + player.score = 0; + computer.score = 0; + playerScoreElement.textContent = '0'; + computerScoreElement.textContent = '0'; + player.y = court.height / 2 - paddle.height / 2; + computer.y = player.y; + resetBall(); +} + +function serve() { + if (!gameStarted) { + gameStarted = true; + messageElement.classList.add('hidden'); + } +} + +function movePlayer() { + const movingUp = keys.has('ArrowUp') || keys.has('w'); + const movingDown = keys.has('ArrowDown') || keys.has('s'); + player.velocity = (movingDown ? paddle.speed : 0) - (movingUp ? paddle.speed : 0); + player.y = Math.max(0, Math.min(court.height - paddle.height, player.y + player.velocity)); +} + +function moveComputer() { + const target = ball.y + ball.size / 2 - paddle.height / 2; + const difference = target - computer.y; + const movement = Math.sign(difference) * Math.min(Math.abs(difference), 5.3); + computer.y = Math.max(0, Math.min(court.height - paddle.height, computer.y + movement)); +} + +function overlaps(paddleObject) { + return ( + ball.x < paddleObject.x + paddle.width && + ball.x + ball.size > paddleObject.x && + ball.y < paddleObject.y + paddle.height && + ball.y + ball.size > paddleObject.y + ); +} + +function bounceFromPaddle(paddleObject, direction) { + const paddleCenter = paddleObject.y + paddle.height / 2; + const ballCenter = ball.y + ball.size / 2; + const impact = (ballCenter - paddleCenter) / (paddle.height / 2); + const nextSpeed = Math.min(ball.maxSpeed, Math.abs(ball.velocityX) + 0.35); + ball.velocityX = direction * nextSpeed; + ball.velocityY = impact * 6; + ball.x = direction === 1 ? paddleObject.x + paddle.width : paddleObject.x - ball.size; +} + +function scorePoint(scoringPlayer) { + scoringPlayer.score += 1; + playerScoreElement.textContent = String(player.score); + computerScoreElement.textContent = String(computer.score); + resetBall(scoringPlayer === player ? 1 : -1); +} + +function update() { + movePlayer(); + moveComputer(); + if (!gameStarted) return; + + ball.x += ball.velocityX; + ball.y += ball.velocityY; + + if (ball.y <= 0 || ball.y + ball.size >= court.height) { + ball.velocityY *= -1; + ball.y = Math.max(0, Math.min(court.height - ball.size, ball.y)); + } + + if (ball.velocityX < 0 && overlaps(player)) bounceFromPaddle(player, 1); + if (ball.velocityX > 0 && overlaps(computer)) bounceFromPaddle(computer, -1); + + if (ball.x + ball.size < 0) scorePoint(computer); + if (ball.x > court.width) scorePoint(player); +} + +function draw() { + context.clearRect(0, 0, court.width, court.height); + context.fillStyle = '#101729'; + context.fillRect(0, 0, court.width, court.height); + + context.setLineDash([8, 14]); + context.strokeStyle = '#36415f'; + context.lineWidth = 3; + context.beginPath(); + context.moveTo(court.width / 2, 18); + context.lineTo(court.width / 2, court.height - 18); + context.stroke(); + context.setLineDash([]); + + context.fillStyle = '#7ee7cc'; + context.fillRect(player.x, player.y, paddle.width, paddle.height); + context.fillStyle = '#f8b26a'; + context.fillRect(computer.x, computer.y, paddle.width, paddle.height); + + context.fillStyle = '#f5f7ff'; + context.beginPath(); + context.arc(ball.x + ball.size / 2, ball.y + ball.size / 2, ball.size / 2, 0, Math.PI * 2); + context.fill(); +} + +function gameLoop() { + update(); + draw(); + animationFrame = requestAnimationFrame(gameLoop); +} + +document.addEventListener('keydown', (event) => { + if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'w', 's'].includes(event.key)) { + event.preventDefault(); + keys.add(event.key); + if (event.key.startsWith('Arrow')) serve(); + } +}); + +document.addEventListener('keyup', (event) => keys.delete(event.key)); +resetButton.addEventListener('click', resetGame); + +resetGame(); +gameLoop(); + +window.addEventListener('beforeunload', () => cancelAnimationFrame(animationFrame)); diff --git a/public/pong/index.html b/public/pong/index.html new file mode 100644 index 000000000000..52bbe34fae9d --- /dev/null +++ b/public/pong/index.html @@ -0,0 +1,43 @@ + + + + + + Pong + + + +
+
+
+

ARCADE CLASSIC

+

Pong

+
+ +
+ +
+
+ You + 0 +
+ : +
+ Computer + 0 +
+
+ +
+ +
Press an arrow key to serve
+
+ +

+ Move your paddle with or W S. + Press to serve again after a point. +

+
+ + + diff --git a/public/pong/styles.css b/public/pong/styles.css new file mode 100644 index 000000000000..00c08b2a759c --- /dev/null +++ b/public/pong/styles.css @@ -0,0 +1,177 @@ +* { + box-sizing: border-box; +} + +:root { + color-scheme: dark; + font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + background: #10131f; + color: #f5f7ff; +} + +body { + min-width: 320px; + min-height: 100vh; + margin: 0; + background: radial-gradient(circle at top, #202943 0, #10131f 58%); +} + +.game-shell { + width: min(960px, calc(100% - 32px)); + margin: 0 auto; + padding: 48px 0 36px; +} + +.game-header, +.scoreboard { + display: flex; + align-items: center; + justify-content: space-between; +} + +.eyebrow { + margin: 0 0 8px; + color: #7ee7cc; + font-size: 0.72rem; + font-weight: 800; + letter-spacing: 0.18em; +} + +h1 { + margin: 0; + font-size: clamp(2.4rem, 7vw, 4.5rem); + letter-spacing: -0.07em; + line-height: 0.9; +} + +.reset-button { + border: 1px solid #495575; + border-radius: 999px; + padding: 10px 18px; + color: #f5f7ff; + background: #202943; + cursor: pointer; + font: inherit; + font-weight: 700; + transition: background 160ms ease, transform 160ms ease; +} + +.reset-button:hover, +.reset-button:focus-visible { + background: #2e3a60; + outline: none; + transform: translateY(-1px); +} + +.scoreboard { + justify-content: center; + gap: 24px; + margin: 38px 0 18px; +} + +.score-card { + display: grid; + gap: 2px; + min-width: 110px; +} + +.computer-score-card { + text-align: right; +} + +.score-label { + color: #aab4d0; + font-size: 0.75rem; + font-weight: 700; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.score-card strong { + font-size: 3rem; + line-height: 1; +} + +.score-divider { + color: #586584; + font-size: 2.5rem; + font-weight: 300; +} + +.court-wrap { + position: relative; + overflow: hidden; + border: 1px solid #3b4664; + border-radius: 18px; + padding: 10px; + background: #090c16; + box-shadow: 0 24px 70px rgba(0, 0, 0, 0.3); +} + +canvas { + display: block; + width: 100%; + height: auto; + border-radius: 10px; + background: #101729; +} + +.game-message { + position: absolute; + top: 50%; + left: 50%; + padding: 9px 15px; + border: 1px solid rgba(126, 231, 204, 0.35); + border-radius: 999px; + color: #d7fff4; + background: rgba(16, 19, 31, 0.78); + font-size: 0.85rem; + font-weight: 700; + transform: translate(-50%, -50%); + transition: opacity 180ms ease; +} + +.game-message.hidden { + opacity: 0; + pointer-events: none; +} + +.instructions { + margin: 18px 0 0; + color: #aab4d0; + font-size: 0.9rem; + text-align: center; +} + +kbd { + display: inline-block; + min-width: 25px; + margin: 0 2px; + padding: 2px 6px; + border: 1px solid #495575; + border-bottom-width: 2px; + border-radius: 5px; + color: #f5f7ff; + background: #202943; + font-size: 0.8rem; + text-align: center; +} + +@media (max-width: 560px) { + .game-shell { + padding-top: 28px; + } + + .game-header { + align-items: flex-start; + } + + .reset-button { + padding: 8px 13px; + font-size: 0.85rem; + } + + .instructions { + line-height: 2; + } +} From 585aad2435ad4f951285b25c253b5e6200c3133d Mon Sep 17 00:00:00 2001 From: homecoc Date: Mon, 21 Sep 2026 18:44:16 +0000 Subject: [PATCH 2/2] Main ### Why: Closes: ### What's being changed (if available, include any code snippets, screenshots, or gifs): ### Check off the following: - [ ] A subject matter expert (SME) has reviewed the technical accuracy of the content in this PR. In most cases, the author can be the SME. Open source contributions may require an SME review from GitHub staff. - [ ] The changes in this PR meet [the docs fundamentals that are required for all content](http://docs.github.com/en/contributing/writing-for-github-docs/about-githubs-documentation-fundamentals). - [ ] All CI checks are passing and the changes look good in the review environment. --- .vscode/launch.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index e7265cc1f844..7526001d701a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,6 +1,16 @@ { "version": "0.2.0", "configurations": [ + { + "name": "Electron Main", + "program": "${workspaceFolder}/main.js", + "request": "launch", + "runtimeExecutable": "electron", + "skipFiles": [ + "/**" + ], + "type": "node" + }, { "type": "node", "request": "attach",