Skip to content

Instantly share code, notes, and snippets.

@Studio1HQ
Last active May 25, 2025 06:32
Show Gist options
  • Save Studio1HQ/155a6565d76a584f69839dbaf66ade4c to your computer and use it in GitHub Desktop.
Save Studio1HQ/155a6565d76a584f69839dbaf66ade4c to your computer and use it in GitHub Desktop.
3d Ping Pong Game by Claude 4
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>3D Tron Pong</title>
<style>
@import url("<https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&display=swap>");
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: #000;
color: #00ffff;
font-family: "Orbitron", monospace;
overflow: hidden;
cursor: none;
}
#gameContainer {
width: 100vw;
height: 100vh;
position: relative;
background: radial-gradient(circle at center, #001122 0%, #000000 100%);
}
#gameCanvas {
display: block;
background: transparent;
}
#ui {
position: absolute;
top: 20px;
left: 50%;
transform: translateX(-50%);
text-align: center;
z-index: 100;
}
#score {
font-size: 24px;
font-weight: 700;
text-shadow: 0 0 10px #00ffff;
margin-bottom: 10px;
}
#instructions {
font-size: 14px;
opacity: 0.8;
text-shadow: 0 0 5px #00ffff;
}
.glow-text {
text-shadow:
0 0 20px #00ffff,
0 0 30px #00ffff,
0 0 40px #00ffff;
}
#gameOver {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
background: rgba(0, 0, 0, 0.8);
padding: 30px;
border: 2px solid #00ffff;
border-radius: 10px;
box-shadow: 0 0 30px #00ffff;
display: none;
z-index: 200;
}
#restartBtn {
background: transparent;
color: #00ffff;
border: 2px solid #00ffff;
padding: 10px 20px;
font-family: "Orbitron", monospace;
cursor: pointer;
margin-top: 15px;
transition: all 0.3s;
}
#restartBtn:hover {
background: #00ffff;
color: #000;
box-shadow: 0 0 20px #00ffff;
}
</style>
</head>
<body>
<div id="gameContainer">
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div id="score">
PLAYER: <span id="playerScore">0</span> | AI:
<span id="aiScore">0</span>
</div>
<div id="instructions">
Move mouse to control paddle • Hit ball edges for spin
</div>
</div>
<div id="gameOver">
<h2 class="glow-text">GAME OVER</h2>
<p id="winner"></p>
<button id="restartBtn">RESTART GAME</button>
</div>
</div>
<script>
const canvas = document.getElementById("gameCanvas");
const ctx = canvas.getContext("2d");
// Set canvas size
function resizeCanvas() {
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
}
resizeCanvas();
window.addEventListener("resize", resizeCanvas);
// Game state
const game = {
playerScore: 0,
aiScore: 0,
isRunning: true,
mouse: { x: 0, y: 0 },
particles: [],
};
// 3D to 2D projection
function project3D(x, y, z) {
const distance = 800;
const scale = distance / (distance + z);
return {
x: canvas.width / 2 + x * scale,
y: canvas.height / 2 + y * scale,
scale: scale,
};
}
// Game objects
const arena = {
width: 600,
height: 400,
depth: 800,
draw() {
ctx.strokeStyle = "#00ffff";
ctx.lineWidth = 2;
ctx.shadowColor = "#00ffff";
ctx.shadowBlur = 10;
// Draw arena bounds
const corners = [
[-this.width / 2, -this.height / 2, -this.depth / 2],
[this.width / 2, -this.height / 2, -this.depth / 2],
[this.width / 2, this.height / 2, -this.depth / 2],
[-this.width / 2, this.height / 2, -this.depth / 2],
[-this.width / 2, -this.height / 2, this.depth / 2],
[this.width / 2, -this.height / 2, this.depth / 2],
[this.width / 2, this.height / 2, this.depth / 2],
[-this.width / 2, this.height / 2, this.depth / 2],
];
const projected = corners.map(([x, y, z]) => project3D(x, y, z));
// Draw back face
ctx.beginPath();
ctx.moveTo(projected[0].x, projected[0].y);
ctx.lineTo(projected[1].x, projected[1].y);
ctx.lineTo(projected[2].x, projected[2].y);
ctx.lineTo(projected[3].x, projected[3].y);
ctx.closePath();
ctx.stroke();
// Draw front face
ctx.beginPath();
ctx.moveTo(projected[4].x, projected[4].y);
ctx.lineTo(projected[5].x, projected[5].y);
ctx.lineTo(projected[6].x, projected[6].y);
ctx.lineTo(projected[7].x, projected[7].y);
ctx.closePath();
ctx.stroke();
// Connect corners
for (let i = 0; i < 4; i++) {
ctx.beginPath();
ctx.moveTo(projected[i].x, projected[i].y);
ctx.lineTo(projected[i + 4].x, projected[i + 4].y);
ctx.stroke();
}
// Draw grid on floor and ceiling
ctx.strokeStyle = "#004466";
ctx.lineWidth = 1;
ctx.shadowBlur = 3;
for (let i = -this.width / 2; i <= this.width / 2; i += 50) {
// Floor grid
const start = project3D(i, this.height / 2, -this.depth / 2);
const end = project3D(i, this.height / 2, this.depth / 2);
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke();
// Ceiling grid
const startCeil = project3D(i, -this.height / 2, -this.depth / 2);
const endCeil = project3D(i, -this.height / 2, this.depth / 2);
ctx.beginPath();
ctx.moveTo(startCeil.x, startCeil.y);
ctx.lineTo(endCeil.x, endCeil.y);
ctx.stroke();
}
for (let i = -this.depth / 2; i <= this.depth / 2; i += 50) {
// Floor grid
const start = project3D(-this.width / 2, this.height / 2, i);
const end = project3D(this.width / 2, this.height / 2, i);
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.lineTo(end.x, end.y);
ctx.stroke();
// Ceiling grid
const startCeil = project3D(-this.width / 2, -this.height / 2, i);
const endCeil = project3D(this.width / 2, -this.height / 2, i);
ctx.beginPath();
ctx.moveTo(startCeil.x, startCeil.y);
ctx.lineTo(endCeil.x, endCeil.y);
ctx.stroke();
}
},
};
const player = {
x: 0,
y: 0,
z: 350,
width: 80,
height: 80,
targetX: 0,
targetY: 0,
update() {
// Smooth movement toward mouse position
const mouseX = (game.mouse.x - canvas.width / 2) * 0.8;
const mouseY = (game.mouse.y - canvas.height / 2) * 0.8;
this.targetX = Math.max(
-arena.width / 2 + this.width / 2,
Math.min(arena.width / 2 - this.width / 2, mouseX),
);
this.targetY = Math.max(
-arena.height / 2 + this.height / 2,
Math.min(arena.height / 2 - this.height / 2, mouseY),
);
this.x += (this.targetX - this.x) * 0.15;
this.y += (this.targetY - this.y) * 0.15;
},
draw() {
ctx.strokeStyle = "#00ff88";
ctx.fillStyle = "rgba(0, 255, 136, 0.3)";
ctx.lineWidth = 3;
ctx.shadowColor = "#00ff88";
ctx.shadowBlur = 15;
const corners = [
[this.x - this.width / 2, this.y - this.height / 2, this.z],
[this.x + this.width / 2, this.y - this.height / 2, this.z],
[this.x + this.width / 2, this.y + this.height / 2, this.z],
[this.x - this.width / 2, this.y + this.height / 2, this.z],
];
const projected = corners.map(([x, y, z]) => project3D(x, y, z));
ctx.beginPath();
ctx.moveTo(projected[0].x, projected[0].y);
for (let i = 1; i < projected.length; i++) {
ctx.lineTo(projected[i].x, projected[i].y);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
},
};
const ai = {
x: 0,
y: 0,
z: -350,
width: 80,
height: 80,
speed: 0.08,
update() {
// AI follows ball with some delay for realistic difficulty
const targetX = ball.x * 0.9;
const targetY = ball.y * 0.9;
this.x += (targetX - this.x) * this.speed;
this.y += (targetY - this.y) * this.speed;
// Keep AI within bounds
this.x = Math.max(
-arena.width / 2 + this.width / 2,
Math.min(arena.width / 2 - this.width / 2, this.x),
);
this.y = Math.max(
-arena.height / 2 + this.height / 2,
Math.min(arena.height / 2 - this.height / 2, this.y),
);
},
draw() {
ctx.strokeStyle = "#ff0088";
ctx.fillStyle = "rgba(255, 0, 136, 0.3)";
ctx.lineWidth = 3;
ctx.shadowColor = "#ff0088";
ctx.shadowBlur = 15;
const corners = [
[this.x - this.width / 2, this.y - this.height / 2, this.z],
[this.x + this.width / 2, this.y - this.height / 2, this.z],
[this.x + this.width / 2, this.y + this.height / 2, this.z],
[this.x - this.width / 2, this.y + this.height / 2, this.z],
];
const projected = corners.map(([x, y, z]) => project3D(x, y, z));
ctx.beginPath();
ctx.moveTo(projected[0].x, projected[0].y);
for (let i = 1; i < projected.length; i++) {
ctx.lineTo(projected[i].x, projected[i].y);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
},
};
const ball = {
x: 0,
y: 0,
z: 0,
vx: 4,
vy: 2,
vz: 8,
spin: { x: 0, y: 0 },
size: 12,
trail: [],
reset() {
this.x = 0;
this.y = 0;
this.z = 0;
this.vx = (Math.random() > 0.5 ? 1 : -1) * (3 + Math.random() * 2);
this.vy = (Math.random() - 0.5) * 4;
this.vz = (Math.random() > 0.5 ? 1 : -1) * (6 + Math.random() * 4);
this.spin = { x: 0, y: 0 };
this.trail = [];
},
update() {
// Apply spin to velocity
this.vx += this.spin.x * 0.1;
this.vy += this.spin.y * 0.1;
// Update position
this.x += this.vx;
this.y += this.vy;
this.z += this.vz;
// Add to trail
this.trail.push({ x: this.x, y: this.y, z: this.z });
if (this.trail.length > 10) this.trail.shift();
// Reduce spin over time
this.spin.x *= 0.98;
this.spin.y *= 0.98;
// Wall collisions
if (
this.x <= -arena.width / 2 + this.size ||
this.x >= arena.width / 2 - this.size
) {
this.vx = -this.vx;
this.x = Math.max(
-arena.width / 2 + this.size,
Math.min(arena.width / 2 - this.size, this.x),
);
this.createImpactParticles();
}
if (
this.y <= -arena.height / 2 + this.size ||
this.y >= arena.height / 2 - this.size
) {
this.vy = -this.vy;
this.y = Math.max(
-arena.height / 2 + this.size,
Math.min(arena.height / 2 - this.size, this.y),
);
this.createImpactParticles();
}
// Paddle collisions
this.checkPaddleCollision(player);
this.checkPaddleCollision(ai);
// Score detection
if (this.z > arena.depth / 2) {
game.aiScore++;
this.reset();
this.createScoreParticles();
} else if (this.z < -arena.depth / 2) {
game.playerScore++;
this.reset();
this.createScoreParticles();
}
// Check win condition
if (game.playerScore >= 5 || game.aiScore >= 5) {
this.endGame();
}
},
checkPaddleCollision(paddle) {
if (
Math.abs(this.z - paddle.z) < 20 &&
this.x > paddle.x - paddle.width / 2 - this.size &&
this.x < paddle.x + paddle.width / 2 + this.size &&
this.y > paddle.y - paddle.height / 2 - this.size &&
this.y < paddle.y + paddle.height / 2 + this.size
) {
this.vz = -this.vz;
this.z = paddle.z + (this.z > paddle.z ? 20 : -20);
// Add spin based on hit position
const hitX = (this.x - paddle.x) / (paddle.width / 2);
const hitY = (this.y - paddle.y) / (paddle.height / 2);
this.spin.x += hitY * 0.5;
this.spin.y += hitX * 0.5;
// Increase speed slightly
this.vz *= 1.05;
this.vx += hitX * 2;
this.vy += hitY * 2;
this.createImpactParticles();
}
},
createImpactParticles() {
for (let i = 0; i < 8; i++) {
game.particles.push({
x: this.x,
y: this.y,
z: this.z,
vx: (Math.random() - 0.5) * 10,
vy: (Math.random() - 0.5) * 10,
vz: (Math.random() - 0.5) * 10,
life: 20,
maxLife: 20,
color: "#ffff00",
});
}
},
createScoreParticles() {
for (let i = 0; i < 20; i++) {
game.particles.push({
x: this.x + (Math.random() - 0.5) * 100,
y: this.y + (Math.random() - 0.5) * 100,
z: this.z + (Math.random() - 0.5) * 100,
vx: (Math.random() - 0.5) * 15,
vy: (Math.random() - 0.5) * 15,
vz: (Math.random() - 0.5) * 15,
life: 30,
maxLife: 30,
color: this.z > 0 ? "#ff0088" : "#00ff88",
});
}
},
endGame() {
game.isRunning = false;
const winner = game.playerScore >= 5 ? "PLAYER WINS!" : "AI WINS!";
document.getElementById("winner").textContent = winner;
document.getElementById("gameOver").style.display = "block";
},
draw() {
// Draw trail
ctx.strokeStyle = "#ffffff";
ctx.lineWidth = 2;
ctx.shadowColor = "#ffffff";
ctx.shadowBlur = 8;
if (this.trail.length > 1) {
ctx.beginPath();
const start = project3D(
this.trail[0].x,
this.trail[0].y,
this.trail[0].z,
);
ctx.moveTo(start.x, start.y);
for (let i = 1; i < this.trail.length; i++) {
const pos = project3D(
this.trail[i].x,
this.trail[i].y,
this.trail[i].z,
);
ctx.globalAlpha = (i / this.trail.length) * 0.5;
ctx.lineTo(pos.x, pos.y);
}
ctx.stroke();
ctx.globalAlpha = 1;
}
// Draw ball
const pos = project3D(this.x, this.y, this.z);
ctx.fillStyle = "#ffffff";
ctx.strokeStyle = "#00ffff";
ctx.lineWidth = 2;
ctx.shadowColor = "#ffffff";
ctx.shadowBlur = 20;
ctx.beginPath();
ctx.arc(pos.x, pos.y, this.size * pos.scale, 0, Math.PI * 2);
ctx.fill();
ctx.stroke();
},
};
// Particle system
function updateParticles() {
for (let i = game.particles.length - 1; i >= 0; i--) {
const p = game.particles[i];
p.x += p.vx;
p.y += p.vy;
p.z += p.vz;
p.life--;
if (p.life <= 0) {
game.particles.splice(i, 1);
}
}
}
function drawParticles() {
game.particles.forEach((p) => {
const pos = project3D(p.x, p.y, p.z);
const alpha = p.life / p.maxLife;
ctx.fillStyle = p.color;
ctx.globalAlpha = alpha;
ctx.shadowColor = p.color;
ctx.shadowBlur = 5;
ctx.beginPath();
ctx.arc(pos.x, pos.y, 3 * pos.scale, 0, Math.PI * 2);
ctx.fill();
});
ctx.globalAlpha = 1;
}
// Input handling
canvas.addEventListener("mousemove", (e) => {
game.mouse.x = e.clientX;
game.mouse.y = e.clientY;
});
// Game loop
function gameLoop() {
// Clear canvas with fade effect
ctx.fillStyle = "rgba(0, 0, 0, 0.1)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
if (game.isRunning) {
// Update
player.update();
ai.update();
ball.update();
updateParticles();
// Draw
arena.draw();
player.draw();
ai.draw();
ball.draw();
drawParticles();
// Update UI
document.getElementById("playerScore").textContent = game.playerScore;
document.getElementById("aiScore").textContent = game.aiScore;
}
requestAnimationFrame(gameLoop);
}
// Restart functionality
document.getElementById("restartBtn").addEventListener("click", () => {
game.playerScore = 0;
game.aiScore = 0;
game.isRunning = true;
game.particles = [];
ball.reset();
document.getElementById("gameOver").style.display = "none";
});
// Initialize game
ball.reset();
gameLoop();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment