Last active
May 25, 2025 06:32
-
-
Save Studio1HQ/155a6565d76a584f69839dbaf66ade4c to your computer and use it in GitHub Desktop.
3d Ping Pong Game by Claude 4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!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