<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Free-Move Pong</title>
<style>
html, body {
margin: 0;
padding: 0;
background: #10131a;
color: #eee;
font-family: 'Trebuchet MS', sans-serif;
overflow: hidden;
height: 100%;
}
#wrap {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
}
h1 {
margin: 8px 0 4px 0;
font-size: 28px;
letter-spacing: 2px;
color: #7CFC9A;
text-shadow: 0 0 8px rgba(124,252,154,0.5);
}
#info {
margin-bottom: 6px;
font-size: 14px;
color: #aaa;
}
canvas {
background: #06080c;
border: 3px solid #444;
border-radius: 12px;
box-shadow: 0 0 25px rgba(0,255,150,0.15);
}
#score {
font-size: 30px;
margin: 6px 0;
font-weight: bold;
}
#score span {
padding: 0 20px;
}
#p1score { color: #4fd1ff; }
#p2score { color: #ff6b6b; }
#msg {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 40px;
font-weight: bold;
color: #ffe066;
text-shadow: 0 0 15px rgba(255,224,102,0.7);
display: none;
text-align: center;
}
#restartBtn {
margin-top: 10px;
padding: 8px 18px;
font-size: 16px;
border-radius: 8px;
border: none;
background: #7CFC9A;
color: #10131a;
font-weight: bold;
cursor: pointer;
display: none;
}
#restartBtn:hover {
background: #a3ffbe;
}
#botBtn {
margin-bottom: 6px;
padding: 6px 14px;
font-size: 13px;
border-radius: 8px;
border: none;
background: #ffd700;
color: #10131a;
font-weight: bold;
cursor: pointer;
}
#botBtn:hover {
background: #ffe680;
}
</style>
</head>
<body>
<div id="wrap">
<h1>FREE-MOVE PONG</h1>
<div id="info">Player 1 (blue): W A S D | Player 2 (red): Arrow Keys | First to 5 wins!</div>
<button id="botBtn">Enable Bot (P2)</button>
<div id="score"><span id="p1score">0</span>:<span id="p2score">0</span></div>
<div style="position:relative;">
<canvas id="game" width="800" height="500"></canvas>
<div id="msg"></div>
</div>
<button id="restartBtn">Play Again</button>
</div>
<script>
const canvas = document.getElementById('game');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PADDLE_W = 16, PADDLE_H = 90, SPEED = 6, BALL_SIZE = 12;
const WIN_SCORE = 5;
let p1 = { x: 30, y: H/2 - PADDLE_H/2, w: PADDLE_W, h: PADDLE_H, score: 0 };
let p2 = { x: W - 30 - PADDLE_W, y: H/2 - PADDLE_H/2, w: PADDLE_W, h: PADDLE_H, score: 0 };
let ball = { x: W/2, y: H/2, r: 15, vx: 5, vy: 3, spin: 0 };
let lastP1 = {x: p1 ? p1.x : 30, y: p1 ? p1.y : H/2};
let lastP2 = {x: 0, y: 0};
let powFlash = 0;
let items = [];
let nextItemTime = 180;
const BASE_PADDLE_H = PADDLE_H;
let starCount = 0;
function spawnItem() {
const canStar = starCount < 2 && Math.random() < 0.15;
const type = canStar ? 'star' : ['grow', 'speed', 'hazard'][Math.floor(Math.random() * 3)];
if (type === 'star') starCount++;
items.push({
type: type,
x: W/2 - 140 + Math.random() * 280 + (Math.random() < 0.5 ? -60 : 60),
y: 40 + Math.random() * (H - 80),
r: 14,
life: 600
});
}
let botEnabled = false;
const botBtn = document.getElementById('botBtn');
botBtn.addEventListener('click', () => {
botEnabled = !botEnabled;
botBtn.textContent = botEnabled ? 'Disable Bot (P2)' : 'Enable Bot (P2)';
});
function runBot(speed) {
let targetX = ball.x, targetY = ball.y;
let bestStar = null, bestDist = Infinity;
items.forEach(it => {
if (it.type === 'star') {
const d = Math.hypot(it.x - (p2.x + p2.w/2), it.y - (p2.y + p2.h/2));
if (d < bestDist) { bestDist = d; bestStar = it; }
}
});
if (bestStar && bestDist < 260) { targetX = bestStar.x; targetY = bestStar.y; }
const cx = p2.x + p2.w/2, cy = p2.y + p2.h/2;
const dx = targetX - cx, dy = targetY - cy;
if (Math.abs(dx) > 4) p2.x += Math.sign(dx) * speed;
if (Math.abs(dy) > 4) p2.y += Math.sign(dy) * speed;
}
let keys = {};
document.addEventListener('keydown', e => {
keys[e.key.toLowerCase()] = true;
if (["arrowup","arrowdown","arrowleft","arrowright"," "].includes(e.key.toLowerCase())) e.preventDefault();
});
document.addEventListener('keyup', e => { keys[e.key.toLowerCase()] = false; });
function resetBall(direction) {
ball.x = W/2;
ball.y = H/2;
let angle = (Math.random() * 0.6 - 0.3);
let speed = 5.5;
ball.vx = Math.cos(angle) * speed * direction;
ball.vy = Math.sin(angle) * speed;
}
function clamp(v, min, max) { return Math.max(min, Math.min(max, v)); }
let gameOver = false;
function applyPowerup(p, type) {
if (type === 'grow') {
p.h = Math.min(BASE_PADDLE_H * 1.6, p.h + 40);
clearTimeout(p.growTimer);
p.growTimer = setTimeout(() => { p.h = BASE_PADDLE_H; }, 7000);
} else if (type === 'speed') {
p.boost = 1.8;
clearTimeout(p.boostTimer);
p.boostTimer = setTimeout(() => { p.boost = 1; }, 5000);
} else if (type === 'hazard') {
p.h = Math.max(40, p.h - 30);
clearTimeout(p.growTimer);
p.growTimer = setTimeout(() => { p.h = BASE_PADDLE_H; }, 4000);
} else if (type === 'star') {
p.h = Math.min(BASE_PADDLE_H * 1.6, p.h + 40);
clearTimeout(p.growTimer);
p.growTimer = setTimeout(() => { p.h = BASE_PADDLE_H; }, 7000);
p.boost = 1.8;
clearTimeout(p.boostTimer);
p.boostTimer = setTimeout(() => { p.boost = 1; }, 5000);
}
}
function update() {
if (gameOver) return;
const s1 = SPEED * (p1.boost || 1);
const prevP1x = p1.x, prevP1y = p1.y;
if (keys['w']) p1.y -= s1;
if (keys['s']) p1.y += s1;
if (keys['a']) p1.x -= s1;
if (keys['d']) p1.x += s1;
const s2 = SPEED * (p2.boost || 1);
const prevP2x = p2.x, prevP2y = p2.y;
if (botEnabled) {
runBot(s2);
} else {
if (keys['arrowup']) p2.y -= s2;
if (keys['arrowdown']) p2.y += s2;
if (keys['arrowleft']) p2.x -= s2;
if (keys['arrowright']) p2.x += s2;
}
p1.x = clamp(p1.x, 5, W - p1.w - 5);
p1.y = clamp(p1.y, 5, H - p1.h - 5);
p2.x = clamp(p2.x, 5, W - p2.w - 5);
p2.y = clamp(p2.y, 5, H - p2.h - 5);
p1.vx_move = p1.x - prevP1x; p1.vy_move = p1.y - prevP1y;
p2.vx_move = p2.x - prevP2x; p2.vy_move = p2.y - prevP2y;
if (powFlash > 0) powFlash--;
nextItemTime--;
if (nextItemTime <= 0 && items.length < 3) {
spawnItem();
nextItemTime = 240 + Math.random() * 240;
}
for (let i = items.length - 1; i >= 0; i--) {
const it = items[i];
it.life--;
if (it.life <= 0) { if (it.type === 'star') starCount--; items.splice(i, 1); continue; }
function hits(p) {
let cx = clamp(it.x, p.x, p.x + p.w);
let cy = clamp(it.y, p.y, p.y + p.h);
let dx = it.x - cx, dy = it.y - cy;
return dx*dx + dy*dy < it.r * it.r;
}
if (hits(p1)) { applyPowerup(p1, it.type); if (it.type === 'star') starCount--; items.splice(i, 1); continue; }
if (hits(p2)) { applyPowerup(p2, it.type); if (it.type === 'star') starCount--; items.splice(i, 1); continue; }
}
const prevBallX = ball.x;
ball.x += ball.vx;
ball.y += ball.vy;
ball.spin += Math.hypot(ball.vx, ball.vy) * 0.035 * (ball.vx < 0 ? -1 : 1);
const FRICTION = 0.994;
const MIN_SPEED = 1.6;
ball.vx *= FRICTION;
ball.vy *= FRICTION;
let curSpeed = Math.hypot(ball.vx, ball.vy);
if (curSpeed < MIN_SPEED && curSpeed > 0.01) {
const scale = MIN_SPEED / curSpeed;
ball.vx *= scale;
ball.vy *= scale;
}
if (ball.y - ball.r < 0) { ball.y = ball.r; ball.vy *= -1; }
if (ball.y + ball.r > H) { ball.y = H - ball.r; ball.vy *= -1; }
function collide(p) {
return ball.x + ball.r > p.x && ball.x - ball.r < p.x + p.w &&
ball.y + ball.r > p.y && ball.y - ball.r < p.y + p.h;
}
function resolvePaddleHit(p, prevBallX, isP1) {
if (!collide(p)) return false;
const cameFromLeft = prevBallX <= p.x;
const cameFromRight = prevBallX >= p.x + p.w;
let fromLeft;
if (cameFromLeft && !cameFromRight) fromLeft = true;
else if (cameFromRight && !cameFromLeft) fromLeft = false;
else fromLeft = isP1;
let relY = (ball.y - (p.y + p.h/2)) / (p.h/2);
let paddleSpeed = Math.hypot(p.vx_move || 0, p.vy_move || 0);
let kick = 6 + paddleSpeed * 1.5;
let speed = Math.min(Math.max(Math.hypot(ball.vx, ball.vy) * 1.06, kick), 20);
let angle = relY * (Math.PI/3.2);
if (fromLeft) {
ball.x = p.x - ball.r;
ball.vx = -Math.cos(angle) * speed;
if (ball.vx > -2) ball.vx = -2;
} else {
ball.x = p.x + p.w + ball.r;
ball.vx = Math.cos(angle) * speed;
if (ball.vx < 2) ball.vx = 2;
}
ball.vy = Math.sin(angle) * speed + (p.vy_move || 0) * 0.3;
if (paddleSpeed > 5) { powFlash = 20; powFlashX = ball.x; powFlashY = ball.y; }
return true;
}
resolvePaddleHit(p1, prevBallX, true);
resolvePaddleHit(p2, prevBallX, false);
if (ball.x < -30) {
p2.score++;
updateScore();
if (p2.score >= WIN_SCORE) { endGame('Player 2 Wins!'); return; }
resetBall(1);
}
if (ball.x > W + 30) {
p1.score++;
updateScore();
if (p1.score >= WIN_SCORE) { endGame('Player 1 Wins!'); return; }
resetBall(-1);
}
}
function updateScore() {
document.getElementById('p1score').textContent = p1.score;
document.getElementById('p2score').textContent = p2.score;
}
function endGame(text) {
gameOver = true;
const msg = document.getElementById('msg');
msg.textContent = text;
msg.style.display = 'block';
document.getElementById('restartBtn').style.display = 'inline-block';
}
document.getElementById('restartBtn').addEventListener('click', () => {
p1.score = 0; p2.score = 0;
updateScore();
p1.x = 30; p1.y = H/2 - PADDLE_H/2; p1.h = BASE_PADDLE_H; p1.boost = 1;
p2.x = W - 30 - PADDLE_W; p2.y = H/2 - PADDLE_H/2; p2.h = BASE_PADDLE_H; p2.boost = 1;
items = [];
starCount = 0;
resetBall(Math.random() < 0.5 ? 1 : -1);
gameOver = false;
document.getElementById('msg').style.display = 'none';
document.getElementById('restartBtn').style.display = 'none';
});
function drawItems() {
items.forEach(it => {
ctx.save();
if (it.type === 'grow') {
ctx.fillStyle = '#7CFC9A';
ctx.shadowColor = '#7CFC9A';
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(it.x, it.y, it.r, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#10131a';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('+', it.x, it.y+1);
} else if (it.type === 'speed') {
ctx.fillStyle = '#ffe066';
ctx.shadowColor = '#ffe066';
ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(it.x, it.y, it.r, 0, Math.PI*2);
ctx.fill();
ctx.fillStyle = '#10131a';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('\u26A1', it.x, it.y+1);
} else if (it.type === 'star') {
ctx.fillStyle = '#ffd700';
ctx.shadowColor = '#ffd700';
ctx.shadowBlur = 20;
ctx.beginPath();
const spikes = 5;
for (let k = 0; k < spikes * 2; k++) {
const ang = (Math.PI * k) / spikes - Math.PI/2;
const rad = k % 2 === 0 ? it.r * 1.4 : it.r * 0.6;
const px = it.x + Math.cos(ang) * rad;
const py = it.y + Math.sin(ang) * rad;
if (k === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
} else {
ctx.fillStyle = '#ff3b3b';
ctx.shadowColor = '#ff3b3b';
ctx.shadowBlur = 12;
ctx.beginPath();
const spikes = 8;
for (let k = 0; k < spikes; k++) {
const ang = (Math.PI * 2 * k) / spikes;
const rad = k % 2 === 0 ? it.r : it.r * 0.5;
const px = it.x + Math.cos(ang) * rad;
const py = it.y + Math.sin(ang) * rad;
if (k === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
}
ctx.restore();
});
ctx.shadowBlur = 0;
}
function draw() {
ctx.clearRect(0, 0, W, H);
ctx.strokeStyle = '#333';
ctx.setLineDash([10, 10]);
ctx.beginPath();
ctx.moveTo(W/2, 0);
ctx.lineTo(W/2, H);
ctx.stroke();
ctx.setLineDash([]);
ctx.strokeStyle = '#4fd1ff';
ctx.lineWidth = 4;
ctx.strokeRect(2, H/2 - 70, 10, 140);
ctx.strokeStyle = '#ff6b6b';
ctx.strokeRect(W - 12, H/2 - 70, 10, 140);
ctx.lineWidth = 1;
ctx.fillStyle = '#4fd1ff';
ctx.shadowColor = '#4fd1ff';
ctx.shadowBlur = 12;
ctx.fillRect(p1.x, p1.y, p1.w, p1.h);
ctx.fillStyle = '#ff6b6b';
ctx.shadowColor = '#ff6b6b';
ctx.fillRect(p2.x, p2.y, p2.w, p2.h);
ctx.save();
ctx.fillStyle = 'rgba(0,0,0,0.35)';
ctx.beginPath();
ctx.ellipse(ball.x, ball.y + ball.r * 0.9, ball.r * 0.9, ball.r * 0.35, 0, 0, Math.PI*2);
ctx.fill();
ctx.restore();
ctx.save();
ctx.translate(ball.x, ball.y);
ctx.rotate(ball.spin);
ctx.shadowColor = '#fff';
ctx.shadowBlur = 10;
ctx.fillStyle = '#f5f5f5';
ctx.beginPath();
ctx.arc(0, 0, ball.r, 0, Math.PI*2);
ctx.fill();
ctx.shadowBlur = 0;
ctx.strokeStyle = '#222';
ctx.lineWidth = 1.4;
ctx.stroke();
ctx.fillStyle = '#1a1a1a';
function pentagon(cx, cy, size, rot) {
ctx.beginPath();
for (let k = 0; k < 5; k++) {
const ang = rot + (Math.PI * 2 * k) / 5 - Math.PI/2;
const px = cx + Math.cos(ang) * size;
const py = cy + Math.sin(ang) * size;
if (k === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
}
pentagon(0, 0, ball.r * 0.42, 0);
for (let k = 0; k < 5; k++) {
const ang = (Math.PI * 2 * k) / 5 - Math.PI/2;
const px = Math.cos(ang) * ball.r * 0.72;
const py = Math.sin(ang) * ball.r * 0.72;
pentagon(px, py, ball.r * 0.28, ang);
}
ctx.restore();
drawItems();
if (powFlash > 0) {
ctx.save();
ctx.globalAlpha = powFlash / 20;
ctx.fillStyle = '#ffe066';
ctx.font = 'bold 26px sans-serif';
ctx.textAlign = 'center';
ctx.shadowColor = '#ffe066';
ctx.shadowBlur = 15;
ctx.fillText('POW!', powFlashX, powFlashY - 25);
ctx.restore();
}
}
function loop() {
update();
draw();
requestAnimationFrame(loop);
}
resetBall(Math.random() < 0.5 ? 1 : -1);
loop();
</script>
</body>
</html>