Spaces:
Running
Running
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Gravity Pong</title> | |
<script src="https://cdn.tailwindcss.com"></script> | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/p5.js"></script> | |
<style> | |
.gravity-slider { | |
-webkit-appearance: none; | |
width: 100%; | |
height: 8px; | |
border-radius: 4px; | |
background: #4b5563; | |
outline: none; | |
} | |
.gravity-slider::-webkit-slider-thumb { | |
-webkit-appearance: none; | |
appearance: none; | |
width: 20px; | |
height: 20px; | |
border-radius: 50%; | |
background: #3b82f6; | |
cursor: pointer; | |
} | |
canvas { | |
display: block; | |
margin: 0 auto; | |
border-radius: 12px; | |
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); | |
} | |
</style> | |
</head> | |
<body class="bg-gray-900 text-white min-h-screen flex flex-col items-center justify-center p-4"> | |
<div class="w-full max-w-4xl"> | |
<h1 class="text-4xl font-bold text-center mb-2 text-blue-400">Gravity Pong</h1> | |
<p class="text-center text-gray-300 mb-6">Play Pong with a twist - navigate through a solar system's gravity!</p> | |
<div id="game-container" class="relative mb-6"></div> | |
<div class="bg-gray-800 p-4 rounded-lg shadow-lg"> | |
<div class="flex flex-col space-y-4"> | |
<div> | |
<label class="block text-sm font-medium text-gray-300 mb-2">Gravity Strength</label> | |
<input type="range" min="0" max="2" step="0.1" value="1" class="gravity-slider" id="gravitySlider"> | |
<div class="flex justify-between text-xs text-gray-400"> | |
<span>Weak</span> | |
<span>Normal</span> | |
<span>Strong</span> | |
</div> | |
</div> | |
<div class="flex justify-between"> | |
<div> | |
<p class="text-sm font-medium text-gray-300">Player Score: <span id="playerScore" class="text-blue-400">0</span></p> | |
</div> | |
<div> | |
<p class="text-sm font-medium text-gray-300">AI Score: <span id="aiScore" class="text-red-400">0</span></p> | |
</div> | |
</div> | |
<div class="pt-2"> | |
<p class="text-xs text-gray-400 text-center">Use <span class="font-bold">W/S</span> or <span class="font-bold">↑/↓</span> keys to move your paddle</p> | |
</div> | |
</div> | |
</div> | |
</div> | |
<script> | |
let ball; | |
let playerPaddle; | |
let aiPaddle; | |
let playerScore = 0; | |
let aiScore = 0; | |
let gravityStrength = 1; | |
let planets = []; | |
let sun; | |
function setup() { | |
const container = document.getElementById('game-container'); | |
const canvas = createCanvas(800, 500); | |
canvas.parent('game-container'); | |
// Initialize game objects | |
ball = new Ball(); | |
playerPaddle = new Paddle(true); | |
aiPaddle = new Paddle(false); | |
// Create solar system | |
sun = { | |
x: width/2, | |
y: height/2, | |
radius: 40, | |
mass: 1000 | |
}; | |
planets = [ | |
{ x: width/2 + 120, y: height/2, radius: 15, mass: 100, angle: 0, distance: 120, speed: 0.02 }, | |
{ x: width/2 - 180, y: height/2, radius: 20, mass: 150, angle: PI, distance: 180, speed: 0.015 }, | |
{ x: width/2, y: height/2 + 100, radius: 10, mass: 50, angle: PI/2, distance: 100, speed: 0.025 } | |
]; | |
// Set initial ball position (offset from center to avoid immediate sun interaction) | |
ball.reset(100, height/2); | |
// Setup slider | |
const slider = document.getElementById('gravitySlider'); | |
slider.addEventListener('input', function() { | |
gravityStrength = parseFloat(this.value); | |
}); | |
} | |
function draw() { | |
background(23, 23, 32); | |
// Draw star (sun) | |
drawSun(); | |
// Update and draw planets | |
updatePlanets(); | |
// Draw gravity fields | |
drawGravityFields(); | |
// Update and display game objects | |
ball.update(); | |
ball.display(); | |
playerPaddle.update(); | |
playerPaddle.display(); | |
aiPaddle.update(); | |
aiPaddle.display(); | |
// Check collisions | |
checkPaddleCollision(ball, playerPaddle); | |
checkPaddleCollision(ball, aiPaddle); | |
// AI movement | |
aiMovement(); | |
// Display scores | |
document.getElementById('playerScore').textContent = playerScore; | |
document.getElementById('aiScore').textContent = aiScore; | |
} | |
function drawSun() { | |
// Sun glow effect | |
for (let i = 0; i < 3; i++) { | |
let alpha = 50 - i * 15; | |
fill(255, 204, 0, alpha); | |
noStroke(); | |
ellipse(sun.x, sun.y, sun.radius * 2 + i * 20); | |
} | |
// Sun core | |
fill(255, 204, 0); | |
stroke(255, 153, 0); | |
strokeWeight(2); | |
ellipse(sun.x, sun.y, sun.radius * 2); | |
// Sun surface details | |
fill(255, 153, 0, 100); | |
noStroke(); | |
for (let i = 0; i < 10; i++) { | |
let angle = random(TWO_PI); | |
let r = random(sun.radius * 0.7, sun.radius); | |
let x = sun.x + cos(angle) * r; | |
let y = sun.y + sin(angle) * r; | |
ellipse(x, y, random(3, 8)); | |
} | |
} | |
function updatePlanets() { | |
for (let planet of planets) { | |
// Update position based on orbit | |
planet.angle += planet.speed; | |
planet.x = sun.x + cos(planet.angle) * planet.distance; | |
planet.y = sun.y + sin(planet.angle) * planet.distance; | |
// Draw planet | |
fill(100, 200, 255); | |
noStroke(); | |
ellipse(planet.x, planet.y, planet.radius * 2); | |
// Add some details | |
fill(70, 170, 220); | |
ellipse(planet.x - planet.radius/3, planet.y - planet.radius/3, planet.radius/2); | |
} | |
} | |
function drawGravityFields() { | |
noFill(); | |
stroke(255, 100); | |
strokeWeight(1); | |
// Sun gravity field | |
ellipse(sun.x, sun.y, sun.radius * 8); | |
// Planet gravity fields | |
for (let planet of planets) { | |
ellipse(planet.x, planet.y, planet.radius * 6); | |
} | |
} | |
function aiMovement() { | |
// Simple AI to follow the ball | |
let aiPaddleCenter = aiPaddle.y + aiPaddle.height / 2; | |
let ballFutureY = ball.y + ball.ySpeed * 3; // Predict ball position | |
if (aiPaddleCenter < ballFutureY - 10) { | |
aiPaddle.move(5); | |
} else if (aiPaddleCenter > ballFutureY + 10) { | |
aiPaddle.move(-5); | |
} | |
} | |
function keyPressed() { | |
if (keyCode === UP_ARROW || key === 'w' || key === 'W') { | |
playerPaddle.move(-10); | |
} else if (keyCode === DOWN_ARROW || key === 's' || key === 'S') { | |
playerPaddle.move(10); | |
} | |
} | |
function keyReleased() { | |
if ((keyCode === UP_ARROW || key === 'w' || key === 'W') || | |
(keyCode === DOWN_ARROW || key === 's' || key === 'S')) { | |
playerPaddle.move(0); | |
} | |
} | |
function checkPaddleCollision(ball, paddle) { | |
if (ball.x - ball.radius < paddle.x + paddle.width && | |
ball.x + ball.radius > paddle.x && | |
ball.y - ball.radius < paddle.y + paddle.height && | |
ball.y + ball.radius > paddle.y) { | |
// Calculate angle based on where ball hits paddle | |
let intersectY = (ball.y - (paddle.y + paddle.height/2)); | |
let normalizedIntersectY = intersectY/(paddle.height/2); | |
let bounceAngle = normalizedIntersectY * PI/4; | |
// Reverse direction and add angle | |
ball.xSpeed = paddle.isLeft ? abs(ball.xSpeed) : -abs(ball.xSpeed); | |
ball.ySpeed = ball.maxSpeed * sin(bounceAngle); | |
// Add some randomness | |
ball.ySpeed += random(-0.5, 0.5); | |
// Ensure speed stays within bounds | |
let speed = sqrt(ball.xSpeed * ball.xSpeed + ball.ySpeed * ball.ySpeed); | |
if (speed > ball.maxSpeed) { | |
ball.xSpeed = ball.xSpeed * ball.maxSpeed / speed; | |
ball.ySpeed = ball.ySpeed * ball.maxSpeed / speed; | |
} | |
// Add visual effect | |
paddle.hitEffect(); | |
} | |
} | |
class Ball { | |
constructor() { | |
this.radius = 10; | |
this.reset(100, height/2); | |
this.maxSpeed = 8; | |
this.color = [255, 255, 255]; | |
} | |
reset(x, y) { | |
this.x = x; | |
this.y = y; | |
this.xSpeed = random(4, 6) * (random() > 0.5 ? 1 : -1); | |
this.ySpeed = random(-2, 2); | |
this.trail = []; | |
} | |
update() { | |
// Apply gravity from sun | |
this.applyGravity(sun); | |
// Apply gravity from planets | |
for (let planet of planets) { | |
this.applyGravity(planet); | |
} | |
{ | |
const dx = this.x - sun.x; | |
const dy = this.y - sun.y; | |
const dist = Math.sqrt(dx*dx + dy*dy); | |
const deflectRadius = sun.radius * 0.6; // ring just outside the sun | |
if (dist < deflectRadius && dist > 0) { | |
// unit outward normal | |
const nx = dx / dist, ny = dy / dist; | |
// reflect velocity across the normal | |
const vdotn = this.xSpeed * nx + this.ySpeed * ny; | |
this.xSpeed -= 2 * vdotn * nx; | |
this.ySpeed -= 2 * vdotn * ny; | |
// push the ball to the ring boundary so it doesn't immediately retrigger | |
this.x = sun.x + nx * deflectRadius; | |
this.y = sun.y + ny * deflectRadius; | |
} | |
} | |
// Update position | |
this.x += this.xSpeed; | |
this.y += this.ySpeed; | |
// Store position for trail | |
this.trail.push({x: this.x, y: this.y}); | |
if (this.trail.length > 20) { | |
this.trail.shift(); | |
} | |
// Check wall collisions | |
if (this.y - this.radius < 0 || this.y + this.radius > height) { | |
this.ySpeed *= -1; | |
} | |
// Check scoring | |
if (this.x - this.radius < 0) { | |
aiScore++; | |
this.reset(width - 100, height/2); | |
} else if (this.x + this.radius > width) { | |
playerScore++; | |
this.reset(100, height/2); | |
} | |
} | |
applyGravity(body) { | |
// Calculate distance to gravitational body | |
let dx = body.x - this.x; | |
let dy = body.y - this.y; | |
let distance = sqrt(dx * dx + dy * dy); | |
// Calculate gravitational force (inverse square law) | |
let force = (body.mass * gravityStrength) / (distance * distance); | |
// Normalize direction vector | |
if (distance > 0) { | |
dx /= distance; | |
dy /= distance; | |
} | |
// Apply force to ball's velocity | |
this.xSpeed += dx * force * 0.5; | |
this.ySpeed += dy * force * 0.5; | |
// Limit speed | |
let speed = sqrt(this.xSpeed * this.xSpeed + this.ySpeed * this.ySpeed); | |
if (speed > this.maxSpeed) { | |
this.xSpeed = this.xSpeed * this.maxSpeed / speed; | |
this.ySpeed = this.ySpeed * this.maxSpeed / speed; | |
} | |
} | |
display() { | |
// Draw trail | |
for (let i = 0; i < this.trail.length; i++) { | |
let alpha = map(i, 0, this.trail.length, 50, 255); | |
let size = map(i, 0, this.trail.length, this.radius * 0.3, this.radius); | |
fill(this.color[0], this.color[1], this.color[2], alpha); | |
noStroke(); | |
ellipse(this.trail[i].x, this.trail[i].y, size * 2); | |
} | |
// Draw ball | |
fill(this.color[0], this.color[1], this.color[2]); | |
stroke(200); | |
strokeWeight(1); | |
ellipse(this.x, this.y, this.radius * 2); | |
// Add some shine | |
fill(255, 255, 255, 150); | |
noStroke(); | |
ellipse(this.x - this.radius/3, this.y - this.radius/3, this.radius/2); | |
} | |
} | |
class Paddle { | |
constructor(isLeft) { | |
this.width = 15; | |
this.height = 100; | |
this.x = isLeft ? 30 : width - 30 - this.width; | |
this.y = height/2 - this.height/2; | |
this.speed = 0; | |
this.isLeft = isLeft; | |
this.color = isLeft ? [100, 200, 255] : [255, 100, 100]; | |
this.hitAlpha = 0; | |
} | |
move(amount) { | |
this.speed = amount; | |
} | |
update() { | |
this.y += this.speed; | |
this.y = constrain(this.y, 0, height - this.height); | |
// Fade hit effect | |
if (this.hitAlpha > 0) { | |
this.hitAlpha -= 5; | |
} | |
} | |
display() { | |
// Glow effect when hit | |
if (this.hitAlpha > 0) { | |
fill(this.color[0], this.color[1], this.color[2], this.hitAlpha); | |
noStroke(); | |
rect(this.x - 5, this.y - 5, this.width + 10, this.height + 10, 5); | |
} | |
// Paddle | |
fill(this.color[0], this.color[1], this.color[2]); | |
stroke(255, 255, 255, 100); | |
strokeWeight(1); | |
rect(this.x, this.y, this.width, this.height, 5); | |
// Paddle details | |
fill(255, 255, 255, 50); | |
noStroke(); | |
rect(this.x + 3, this.y + 10, this.width - 6, this.height - 20, 3); | |
} | |
hitEffect() { | |
this.hitAlpha = 100; | |
} | |
} | |
</script> | |
</body> | |
</html> | |