← back to Games Agentabrams
games/pong/index.html
294 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<title>Abrams Pong 2P</title>
<style>
:root { --accent:#7c5cff; --accent2:#ff4d9d; --ink:#e8e9ef; }
* { box-sizing:border-box; }
html,body { height:100%; margin:0; }
body {
background:#0e1016; color:var(--ink);
font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
overflow:hidden; display:flex; align-items:center; justify-content:center;
user-select:none; -webkit-user-select:none; touch-action:none;
}
#wrap { position:relative; width:100%; height:100%; }
canvas { display:block; width:100%; height:100%; }
#hud {
position:absolute; top:10px; left:50%; transform:translateX(-50%);
display:flex; gap:8px; align-items:center; font-size:11px; letter-spacing:.5px;
background:rgba(20,23,31,.72); border:1px solid #2b3140; border-radius:999px;
padding:6px 12px; backdrop-filter:blur(6px); z-index:3;
}
#hud button {
background:none; border:1px solid #3d4763; color:var(--ink); font:inherit;
font-size:11px; border-radius:999px; padding:3px 9px; cursor:pointer;
}
#hud button:hover { background:#222839; }
#hud button.on { background:var(--accent); border-color:var(--accent); }
#msg {
position:absolute; inset:0; display:flex; flex-direction:column; gap:12px;
align-items:center; justify-content:center; text-align:center; z-index:2;
background:rgba(14,16,22,.55); pointer-events:none;
}
#msg h2 { margin:0; font-size:26px; letter-spacing:2px; }
#msg p { margin:0; font-size:12px; opacity:.6; line-height:1.9; max-width:340px; }
#msg .go { color:var(--accent); font-size:12px; opacity:1; margin-top:6px; }
.hidden { display:none !important; }
</style>
</head>
<body>
<div id="wrap">
<canvas id="c"></canvas>
<div id="hud">
<span id="mode-lbl">2 PLAYER</span>
<button id="mode">vs CPU</button>
<button id="pause">⏸</button>
<button id="mute">🔊</button>
</div>
<div id="msg">
<h2 id="title">ABRAMS PONG</h2>
<p id="sub">Left: <b>W / S</b> · Right: <b>↑ / ↓</b><br>Touch: drag your own half of the screen<br>First to 11 wins</p>
<p class="go" id="go">Press SPACE or tap to serve</p>
</div>
</div>
<script>
(function () {
var cv = document.getElementById('c'), ctx = cv.getContext('2d');
var msg = document.getElementById('msg'), titleEl = document.getElementById('title');
var subEl = document.getElementById('sub'), goEl = document.getElementById('go');
var modeBtn = document.getElementById('mode'), modeLbl = document.getElementById('mode-lbl');
var pauseBtn = document.getElementById('pause'), muteBtn = document.getElementById('mute');
var W = 0, H = 0, dpr = 1;
function resize() {
dpr = Math.min(window.devicePixelRatio || 1, 2);
W = cv.clientWidth; H = cv.clientHeight;
cv.width = Math.floor(W * dpr); cv.height = Math.floor(H * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
}
window.addEventListener('resize', resize); resize();
// ---- audio (procedural, zero-dep) ----
var AC = null, muted = false;
try { muted = localStorage.getItem('aa-pong-mute') === '1'; } catch (e) {}
function actx() { if (!AC) { try { AC = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) {} } return AC; }
function beep(freq, dur, type, vol) {
if (muted) return; var a = actx(); if (!a) return;
if (a.state === 'suspended') a.resume();
var o = a.createOscillator(), g = a.createGain();
o.type = type || 'square'; o.frequency.value = freq;
g.gain.value = vol || 0.06;
o.connect(g); g.connect(a.destination);
var t = a.currentTime; o.start(t);
g.gain.exponentialRampToValueAtTime(0.0001, t + (dur || 0.06));
o.stop(t + (dur || 0.06) + 0.02);
}
function updMute() { muteBtn.textContent = muted ? '🔇' : '🔊'; }
updMute();
// ---- state ----
var WIN = 11;
var vsCPU = false;
var padH = 92, padW = 12, ballR = 8;
var L = { y: 0, score: 0 }, R = { y: 0, score: 0 };
var ball = { x: 0, y: 0, vx: 0, vy: 0, speed: 0 };
var state = 'idle'; // idle | serve | play | point | over
var paused = false, server = 1, winner = 0, flash = 0;
var keys = {};
function centerPads() { L.y = H / 2 - padH / 2; R.y = H / 2 - padH / 2; }
function resetBall(dir) {
ball.x = W / 2; ball.y = H / 2;
ball.speed = Math.max(4.2, W * 0.006);
var ang = (Math.random() * 0.7 - 0.35);
ball.vx = Math.cos(ang) * ball.speed * dir;
ball.vy = Math.sin(ang) * ball.speed;
}
function newGame() {
L.score = 0; R.score = 0; winner = 0;
centerPads(); server = Math.random() < 0.5 ? 1 : -1;
state = 'serve'; showServe();
}
function showServe() {
titleEl.textContent = (L.score + R.score === 0) ? 'ABRAMS PONG' : (L.score + ' — ' + R.score);
subEl.style.display = 'none';
goEl.textContent = 'Press SPACE or tap to serve';
msg.classList.remove('hidden');
}
function hideMsg() { msg.classList.add('hidden'); }
function serve() {
if (state !== 'serve') return;
resetBall(server); state = 'play'; hideMsg();
if (actx() && actx().state === 'suspended') actx().resume();
beep(440, 0.05);
}
function point(side) {
if (side === 1) L.score++; else R.score++;
beep(180, 0.14, 'sawtooth', 0.05); flash = 8;
if (L.score >= WIN || R.score >= WIN) {
winner = L.score > R.score ? 1 : -1; state = 'over';
titleEl.textContent = (winner === 1 ? 'LEFT' : (vsCPU ? 'CPU' : 'RIGHT')) + ' WINS';
subEl.style.display = 'block';
subEl.innerHTML = 'Final ' + L.score + ' — ' + R.score;
goEl.textContent = 'Press SPACE or tap to play again';
msg.classList.remove('hidden');
beep(660, 0.18, 'triangle', 0.06);
setTimeout(function(){ beep(880, 0.22, 'triangle', 0.06); }, 120);
} else {
server = -side; // loser serves
state = 'serve'; showServe();
}
}
// ---- input ----
window.addEventListener('keydown', function (e) {
var k = e.key;
if (k === 'w' || k === 'W' || k === 's' || k === 'S' ||
k === 'ArrowUp' || k === 'ArrowDown' || k === ' ') e.preventDefault();
keys[k.toLowerCase()] = true;
if (k === ' ') { if (state === 'serve') serve(); else if (state === 'over') newGame(); }
if (k === 'p' || k === 'P') togglePause();
if (k === 'm' || k === 'M') toggleMute();
});
window.addEventListener('keyup', function (e) { keys[e.key.toLowerCase()] = false; });
function togglePause() {
if (state !== 'play') return;
paused = !paused; pauseBtn.classList.toggle('on', paused);
pauseBtn.textContent = paused ? '▶' : '⏸';
if (paused) { titleEl.textContent = 'PAUSED'; subEl.style.display='none'; goEl.textContent='Press P to resume'; msg.classList.remove('hidden'); }
else hideMsg();
}
function toggleMute() {
muted = !muted; updMute();
try { localStorage.setItem('aa-pong-mute', muted ? '1' : '0'); } catch (e) {}
}
pauseBtn.onclick = togglePause;
muteBtn.onclick = toggleMute;
modeBtn.onclick = function () {
vsCPU = !vsCPU;
modeLbl.textContent = vsCPU ? '1 P vs CPU' : '2 PLAYER';
modeBtn.textContent = vsCPU ? '2 Player' : 'vs CPU';
modeBtn.classList.toggle('on', vsCPU);
newGame();
};
// ---- touch: left half controls left paddle, right half controls right ----
var touches = {};
function handleTouch(e) {
e.preventDefault();
for (var i = 0; i < e.touches.length; i++) {
var t = e.touches[i];
var rect = cv.getBoundingClientRect();
var x = t.clientX - rect.left, y = t.clientY - rect.top;
if (x < W / 2) L.y = y - padH / 2;
else if (!vsCPU) R.y = y - padH / 2;
}
}
cv.addEventListener('touchstart', function (e) {
if (state === 'serve') serve(); else if (state === 'over') newGame();
handleTouch(e);
}, { passive: false });
cv.addEventListener('touchmove', handleTouch, { passive: false });
// mouse fallback for the message overlay / desktop serve
cv.addEventListener('mousedown', function () {
if (state === 'serve') serve(); else if (state === 'over') newGame();
});
// ---- update ----
function clampPad(p) { if (p.y < 0) p.y = 0; if (p.y > H - padH) p.y = H - padH; }
function step() {
var sp = Math.max(6, H * 0.011);
if (keys['w']) L.y -= sp; if (keys['s']) L.y += sp;
if (!vsCPU) { if (keys['arrowup']) R.y -= sp; if (keys['arrowdown']) R.y += sp; }
else {
// CPU: track ball with capped speed + a little lag
var target = ball.vx > 0 ? ball.y - padH / 2 : H / 2 - padH / 2;
var diff = target - R.y, cap = sp * 0.82;
R.y += Math.max(-cap, Math.min(cap, diff));
}
clampPad(L); clampPad(R);
if (state !== 'play' || paused) return;
ball.x += ball.vx; ball.y += ball.vy;
// top / bottom walls
if (ball.y - ballR < 0) { ball.y = ballR; ball.vy = Math.abs(ball.vy); beep(520, 0.03); }
if (ball.y + ballR > H) { ball.y = H - ballR; ball.vy = -Math.abs(ball.vy); beep(520, 0.03); }
// left paddle
if (ball.vx < 0 && ball.x - ballR < 24 + padW && ball.x - ballR > 12 &&
ball.y > L.y && ball.y < L.y + padH) {
bounce(L, 24 + padW, 1);
}
// right paddle
if (ball.vx > 0 && ball.x + ballR > W - 24 - padW && ball.x + ballR < W - 12 &&
ball.y > R.y && ball.y < R.y + padH) {
bounce(R, W - 24 - padW, -1);
}
// out of bounds → point
if (ball.x < -20) point(-1);
else if (ball.x > W + 20) point(1);
}
function bounce(pad, edgeX, dir) {
var rel = (ball.y - (pad.y + padH / 2)) / (padH / 2); // -1..1
rel = Math.max(-1, Math.min(1, rel));
ball.speed = Math.min(ball.speed * 1.045, W * 0.02);
var ang = rel * 1.05; // up to ~60°
ball.vx = Math.cos(ang) * ball.speed * dir;
ball.vy = Math.sin(ang) * ball.speed;
ball.x = edgeX + dir * (ballR + 1);
beep(680, 0.04, 'square', 0.05);
}
// ---- draw ----
function draw() {
ctx.clearRect(0, 0, W, H);
// subtle vignette bg
ctx.fillStyle = '#0e1016'; ctx.fillRect(0, 0, W, H);
// center dashed net
ctx.strokeStyle = 'rgba(124,92,255,.28)'; ctx.lineWidth = 3;
ctx.setLineDash([10, 14]); ctx.beginPath();
ctx.moveTo(W / 2, 0); ctx.lineTo(W / 2, H); ctx.stroke(); ctx.setLineDash([]);
// scores
ctx.fillStyle = 'rgba(232,233,239,.16)';
ctx.font = '700 ' + Math.min(120, H * 0.22) + 'px ui-monospace, monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(L.score, W * 0.28, H * 0.24);
ctx.fillText(R.score, W * 0.72, H * 0.24);
// paddles
ctx.fillStyle = '#37c7ff'; roundRect(24, L.y, padW, padH, 6); ctx.fill();
ctx.fillStyle = '#ff4d9d'; roundRect(W - 24 - padW, R.y, padW, padH, 6); ctx.fill();
// ball
if (state === 'play' || state === 'point') {
ctx.fillStyle = flash > 0 ? '#fff' : '#ffd23e';
ctx.beginPath(); ctx.arc(ball.x, ball.y, ballR, 0, Math.PI * 2); ctx.fill();
}
if (flash > 0) flash--;
}
function roundRect(x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y); ctx.arcTo(x + w, y, x + w, y + h, r);
ctx.arcTo(x + w, y + h, x, y + h, r); ctx.arcTo(x, y + h, x, y, r);
ctx.arcTo(x, y, x + w, y, r); ctx.closePath();
}
function loop() { step(); draw(); requestAnimationFrame(loop); }
centerPads(); newGame(); loop();
window.focus();
})();
</script>
</body>
</html>