import React, { useState, useEffect, useRef, useCallback } from 'react'; import ReactDOM from 'react-dom/client'; // --- TYPES --- enum GameMode { SINGLE = 'SINGLE', MULTI = 'MULTI', } enum GameState { MENU = 'MENU', PLAYING = 'PLAYING', GAME_OVER = 'GAME_OVER', LEADERBOARD = 'LEADERBOARD', } interface Player { score: number; y: number; height: number; color: string; } interface Ball { x: number; y: number; dx: number; dy: number; size: number; color: string; } interface LeaderboardEntry { name: string; time: number; date: string; } interface GameSettings { winningScore: number; paddleSpeed: number; initialBallSpeed: number; rallyThreshold: number; powerUpDuration: number; } // --- CONSTANTS --- const CANVAS_WIDTH = 800; const CANVAS_HEIGHT = 600; const PADDLE_WIDTH = 15; const INITIAL_PADDLE_HEIGHT = 100; const POWERUP_PADDLE_HEIGHT = 133; const SETTINGS: GameSettings = { winningScore: 10, paddleSpeed: 8, initialBallSpeed: 6, rallyThreshold: 23, powerUpDuration: 15, }; const STORAGE_KEY = 'pong_leaderboard_capoli'; const getRandomColor = (): string => { const h = Math.floor(Math.random() * 360); const s = Math.floor(Math.random() * 20) + 80; const l = Math.floor(Math.random() * 20) + 50; return `hsl(${h}, ${s}%, ${l}%)`; }; // --- UTILS --- const getLeaderboard = (): LeaderboardEntry[] => { try { const data = localStorage.getItem(STORAGE_KEY); return data ? JSON.parse(data) : []; } catch (e) { console.error("Failed to load leaderboard", e); return []; } }; const saveScoreToStorage = (name: string, time: number) => { const currentLeaderboard = getLeaderboard(); const newEntry: LeaderboardEntry = { name: name.substring(0, 8), time, date: new Date().toLocaleDateString('en-US'), }; const updatedLeaderboard = [...currentLeaderboard, newEntry] .sort((a, b) => a.time - b.time) .slice(0, 20); localStorage.setItem(STORAGE_KEY, JSON.stringify(updatedLeaderboard)); }; // --- COMPONENTS --- // Button Component const Button: React.FC & { variant?: 'primary' | 'secondary' }> = ({ children, variant = 'primary', className = '', ...props }) => { const baseStyle = "px-6 py-3 rounded text-lg font-bold transition-transform active:scale-95 font-arcade border-2"; const variants = { primary: "bg-white text-black border-white hover:bg-gray-200", secondary: "bg-black text-white border-white hover:bg-gray-900", }; return ( ); }; // GameCanvas Component interface GameCanvasProps { mode: GameMode; onGameOver: (winner: 1 | 2, finalTime: number) => void; } const GameCanvas: React.FC = ({ mode, onGameOver }) => { const canvasRef = useRef(null); const requestRef = useRef(0); const startTimeRef = useRef(Date.now()); const p1Ref = useRef({ score: 0, y: CANVAS_HEIGHT / 2 - INITIAL_PADDLE_HEIGHT / 2, height: INITIAL_PADDLE_HEIGHT, color: '#FFFFFF' }); const p2Ref = useRef({ score: 0, y: CANVAS_HEIGHT / 2 - INITIAL_PADDLE_HEIGHT / 2, height: INITIAL_PADDLE_HEIGHT, color: '#FFFFFF' }); const ballRef = useRef({ x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT / 2, dx: SETTINGS.initialBallSpeed * (Math.random() > 0.5 ? 1 : -1), dy: SETTINGS.initialBallSpeed * (Math.random() > 0.5 ? 1 : -1), size: 10, color: '#FFFFFF' }); const keysPressed = useRef<{ [key: string]: boolean }>({}); const rallyCount = useRef(0); const powerUpHitsRemaining = useRef(0); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { keysPressed.current[e.key.toLowerCase()] = true; }; const handleKeyUp = (e: KeyboardEvent) => { keysPressed.current[e.key.toLowerCase()] = false; }; const handleKeyScroll = (e: KeyboardEvent) => { if(["ArrowUp","ArrowDown"," "].indexOf(e.code) > -1) { e.preventDefault(); } }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('keyup', handleKeyUp); window.addEventListener('keydown', handleKeyScroll, false); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keyup', handleKeyUp); window.removeEventListener('keydown', handleKeyScroll); }; }, []); const resetBall = useCallback((scorer: 1 | 2) => { ballRef.current = { x: CANVAS_WIDTH / 2, y: CANVAS_HEIGHT / 2, dx: SETTINGS.initialBallSpeed * (scorer === 1 ? -1 : 1), dy: (Math.random() * 4 - 2) + (Math.random() > 0.5 ? 2 : -2), size: 10, color: '#FFFFFF' }; p1Ref.current.color = '#FFFFFF'; p2Ref.current.color = '#FFFFFF'; rallyCount.current = 0; powerUpHitsRemaining.current = 0; p1Ref.current.height = INITIAL_PADDLE_HEIGHT; p2Ref.current.height = INITIAL_PADDLE_HEIGHT; }, []); const changeColors = () => { const newColor = getRandomColor(); ballRef.current.color = newColor; p1Ref.current.color = newColor; p2Ref.current.color = newColor; }; const checkPowerUp = () => { if (powerUpHitsRemaining.current > 0) { powerUpHitsRemaining.current--; if (powerUpHitsRemaining.current === 0) { p1Ref.current.height = INITIAL_PADDLE_HEIGHT; p2Ref.current.height = INITIAL_PADDLE_HEIGHT; } } else { if (rallyCount.current >= SETTINGS.rallyThreshold) { p1Ref.current.height = POWERUP_PADDLE_HEIGHT; p2Ref.current.height = POWERUP_PADDLE_HEIGHT; powerUpHitsRemaining.current = SETTINGS.powerUpDuration; } } }; const update = () => { const p1 = p1Ref.current; const p2 = p2Ref.current; const ball = ballRef.current; // Player 1 Movement if (mode === GameMode.MULTI) { if (keysPressed.current['w']) p1.y -= SETTINGS.paddleSpeed; if (keysPressed.current['s']) p1.y += SETTINGS.paddleSpeed; } else { // Single Player: Controlled by arrows if (keysPressed.current['arrowup']) p1.y -= SETTINGS.paddleSpeed; if (keysPressed.current['arrowdown']) p1.y += SETTINGS.paddleSpeed; } // Player 2 Movement if (mode === GameMode.MULTI) { if (keysPressed.current['arrowup']) p2.y -= SETTINGS.paddleSpeed; if (keysPressed.current['arrowdown']) p2.y += SETTINGS.paddleSpeed; } else { // AI const centerP2 = p2.y + p2.height / 2; if (ball.dx > 0) { if (centerP2 < ball.y - 10) p2.y += SETTINGS.paddleSpeed * 0.85; else if (centerP2 > ball.y + 10) p2.y -= SETTINGS.paddleSpeed * 0.85; } } p1.y = Math.max(0, Math.min(CANVAS_HEIGHT - p1.height, p1.y)); p2.y = Math.max(0, Math.min(CANVAS_HEIGHT - p2.height, p2.y)); ball.x += ball.dx; ball.y += ball.dy; if (ball.y <= 0 || ball.y + ball.size >= CANVAS_HEIGHT) { ball.dy *= -1; } // P1 Collision if ( ball.x <= PADDLE_WIDTH + 10 && ball.x >= PADDLE_WIDTH - 10 && ball.y + ball.size >= p1.y && ball.y <= p1.y + p1.height ) { ball.dx = Math.abs(ball.dx); ball.dx *= 1.025; // 2.5% increase ball.dy *= 1.025; rallyCount.current++; changeColors(); checkPowerUp(); } // P2 Collision if ( ball.x + ball.size >= CANVAS_WIDTH - PADDLE_WIDTH - 10 && ball.x + ball.size <= CANVAS_WIDTH - PADDLE_WIDTH + 10 && ball.y + ball.size >= p2.y && ball.y <= p2.y + p2.height ) { ball.dx = -Math.abs(ball.dx); ball.dx *= 1.025; // 2.5% increase ball.dy *= 1.025; rallyCount.current++; changeColors(); checkPowerUp(); } // Scoring if (ball.x < 0) { p2.score++; if (p2.score >= SETTINGS.winningScore) { const duration = (Date.now() - startTimeRef.current) / 1000; onGameOver(2, duration); } else { resetBall(2); } } else if (ball.x > CANVAS_WIDTH) { p1.score++; if (p1.score >= SETTINGS.winningScore) { const duration = (Date.now() - startTimeRef.current) / 1000; onGameOver(1, duration); } else { resetBall(1); } } }; const draw = (ctx: CanvasRenderingContext2D) => { ctx.fillStyle = '#000000'; ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); ctx.strokeStyle = '#333'; ctx.setLineDash([10, 15]); ctx.beginPath(); ctx.moveTo(CANVAS_WIDTH / 2, 0); ctx.lineTo(CANVAS_WIDTH / 2, CANVAS_HEIGHT); ctx.stroke(); ctx.setLineDash([]); ctx.font = '40px "Press Start 2P"'; ctx.fillStyle = '#666'; ctx.fillText(p1Ref.current.score.toString(), CANVAS_WIDTH / 4, 80); ctx.fillText(p2Ref.current.score.toString(), (CANVAS_WIDTH / 4) * 3, 80); const elapsed = Math.floor((Date.now() - startTimeRef.current) / 1000); ctx.font = '16px "Roboto"'; ctx.fillStyle = '#888'; ctx.fillText(`TIME: ${elapsed}s`, CANVAS_WIDTH / 2 - 40, 30); ctx.fillText(`RALLY: ${rallyCount.current}`, CANVAS_WIDTH / 2 - 40, 50); ctx.fillStyle = p1Ref.current.color; ctx.fillRect(10, p1Ref.current.y, PADDLE_WIDTH, p1Ref.current.height); ctx.fillStyle = p2Ref.current.color; ctx.fillRect(CANVAS_WIDTH - PADDLE_WIDTH - 10, p2Ref.current.y, PADDLE_WIDTH, p2Ref.current.height); ctx.fillStyle = ballRef.current.color; ctx.fillRect(ballRef.current.x, ballRef.current.y, ballRef.current.size, ballRef.current.size); }; const loop = () => { update(); const ctx = canvasRef.current?.getContext('2d'); if (ctx) { draw(ctx); } requestRef.current = requestAnimationFrame(loop); }; useEffect(() => { requestRef.current = requestAnimationFrame(loop); return () => cancelAnimationFrame(requestRef.current); }, []); return (
); }; // Leaderboard Component interface LeaderboardProps { onBack: () => void; } const Leaderboard: React.FC = ({ onBack }) => { const scores = getLeaderboard(); return (

LEADERBOARD

{scores.length === 0 ? ( ) : ( scores.map((entry, index) => ( )) )}
# NAME TIME (s) DATE
No records yet. Be the first!
{index + 1}. {entry.name} {entry.time.toFixed(1)} {entry.date}
); }; // GameOverModal Component interface GameOverModalProps { winner: 1 | 2; time: number; mode: GameMode; onSave: (name: string) => void; onRestart: () => void; } const GameOverModal: React.FC = ({ winner, time, mode, onSave, onRestart }) => { const [name, setName] = useState(''); const [error, setError] = useState(''); const isSinglePlayer = mode === GameMode.SINGLE; // Save only if user won in single player, or always in multiplayer const canSave = !isSinglePlayer || winner === 1; let winnerDisplay = `PLAYER ${winner}`; if (isSinglePlayer) { winnerDisplay = winner === 1 ? 'YOU' : 'CPU'; } const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (!name.trim()) { setError('Enter name'); return; } if (name.length > 8) { setError('Max 8 chars'); return; } onSave(name); }; return (

GAME OVER

WINNER: {winnerDisplay}

TIME: {time.toFixed(2)} seconds

{canSave ? (
{ setName(e.target.value.toUpperCase()); setError(''); }} maxLength={8} className="bg-gray-900 border-b-2 border-white text-center text-2xl p-2 text-white focus:outline-none focus:border-yellow-400 font-mono tracking-widest uppercase" placeholder={isSinglePlayer ? "PLAYER" : `PLAYER ${winner}`} autoFocus /> {error &&

{error}

}
) : (
Better luck next time!
)}
); }; // --- APP --- const App: React.FC = () => { const [gameState, setGameState] = useState(GameState.MENU); const [mode, setMode] = useState(GameMode.SINGLE); const [lastGameResult, setLastGameResult] = useState<{ winner: 1 | 2; time: number } | null>(null); const startGame = (selectedMode: GameMode) => { setMode(selectedMode); setGameState(GameState.PLAYING); }; const handleGameOver = (winner: 1 | 2, time: number) => { setLastGameResult({ winner, time }); setGameState(GameState.GAME_OVER); }; const handleSaveScore = (name: string) => { if (lastGameResult) { saveScoreToStorage(name, lastGameResult.time); setGameState(GameState.LEADERBOARD); } }; return (
{gameState === GameState.MENU && (

PONG

1 Player Controls: Arrows Up/Down

2 Players Controls: P1 (W/S) - P2 (Arrows)


BACK
)} {gameState === GameState.PLAYING && ( )} {gameState === GameState.GAME_OVER && lastGameResult && ( setGameState(GameState.MENU)} /> )} {gameState === GameState.LEADERBOARD && ( setGameState(GameState.MENU)} /> )}
); }; // --- RENDER --- const rootElement = document.getElementById('root'); if (!rootElement) { throw new Error("Could not find root element to mount to"); } const root = ReactDOM.createRoot(rootElement); root.render( );