const express = require('express'); const http = require('http'); const socketIo = require('socket.io'); const path = require('path'); const app = express(); const server = http.createServer(app); const io = socketIo(server, { cors: { origin: "*", methods: ["GET", "POST"], credentials: true }, transports: ['websocket', 'polling'], allowEIO3: true, pingTimeout: 60000, pingInterval: 25000 }); app.get('/health', (req, res) => { res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); }); app.use(express.static(path.join(__dirname, 'public'))); const rooms = new Map(); const COIN_RADIUS = 12; const STRIKER_RADIUS = 18; const BOARD_WIDTH = 520; const BOARD_HEIGHT = 520; const FRICTION = 0.985; const WALL_BOUNCE = 0.85; const STRIKER_LINE_Y1 = BOARD_HEIGHT - 50; const STRIKER_LINE_Y2 = 50; const STRIKER_MIN_X = 60; const STRIKER_MAX_X = BOARD_WIDTH - 60; const POCKET_RADIUS = 25; const POCKETS = [ { x: 0, y: 0 }, { x: BOARD_WIDTH, y: 0 }, { x: 0, y: BOARD_HEIGHT }, { x: BOARD_WIDTH, y: BOARD_HEIGHT } ]; function createInitialState() { const coins = []; coins.push({ id: 'striker', type: 'striker', x: BOARD_WIDTH / 2, y: STRIKER_LINE_Y1, vx: 0, vy: 0, radius: STRIKER_RADIUS, pocketed: false }); coins.push({ id: 'queen', type: 'queen', x: BOARD_WIDTH / 2, y: BOARD_HEIGHT / 2, vx: 0, vy: 0, radius: COIN_RADIUS, pocketed: false }); const whitePositions = [ { x: 0, y: -35 }, { x: 30, y: -17 }, { x: 30, y: 17 }, { x: 0, y: 35 }, { x: -30, y: 17 }, { x: -30, y: -17 }, { x: 0, y: -70 }, { x: 60, y: -35 }, { x: 60, y: 35 } ]; whitePositions.forEach((pos, i) => { coins.push({ id: `white${i}`, type: 'white', x: BOARD_WIDTH / 2 + pos.x, y: BOARD_HEIGHT / 2 + pos.y, vx: 0, vy: 0, radius: COIN_RADIUS, pocketed: false }); }); const blackPositions = [ { x: 0, y: -52 }, { x: 45, y: -26 }, { x: 45, y: 26 }, { x: 0, y: 52 }, { x: -45, y: 26 }, { x: -45, y: -26 }, { x: 52, y: 0 }, { x: -52, y: 0 }, { x: 26, y: -45 } ]; blackPositions.forEach((pos, i) => { coins.push({ id: `black${i}`, type: 'black', x: BOARD_WIDTH / 2 + pos.x, y: BOARD_HEIGHT / 2 + pos.y, vx: 0, vy: 0, radius: COIN_RADIUS, pocketed: false }); }); return { coins, currentPlayer: 1, scores: [0, 0], queenOwner: null, queenNeedsCover: false, gameOver: false, winner: null, isMoving: false, turnAgain: false, foulCommitted: false, lastPocketed: [] }; } function updatePhysics(state) { let moving = false; state.lastPocketed = []; state.coins.forEach(coin => { if (coin.pocketed) return; coin.x += coin.vx; coin.y += coin.vy; coin.vx *= FRICTION; coin.vy *= FRICTION; if (Math.abs(coin.vx) < 0.05) coin.vx = 0; if (Math.abs(coin.vy) < 0.05) coin.vy = 0; if (coin.vx !== 0 || coin.vy !== 0) moving = true; if (coin.x - coin.radius < 0) { coin.x = coin.radius; coin.vx = -coin.vx * WALL_BOUNCE; } if (coin.x + coin.radius > BOARD_WIDTH) { coin.x = BOARD_WIDTH - coin.radius; coin.vx = -coin.vx * WALL_BOUNCE; } if (coin.y - coin.radius < 0) { coin.y = coin.radius; coin.vy = -coin.vy * WALL_BOUNCE; } if (coin.y + coin.radius > BOARD_HEIGHT) { coin.y = BOARD_HEIGHT - coin.radius; coin.vy = -coin.vy * WALL_BOUNCE; } POCKETS.forEach(pocket => { const dist = Math.hypot(coin.x - pocket.x, coin.y - pocket.y); if (dist < POCKET_RADIUS && coin.type !== 'striker') { coin.pocketed = true; coin.vx = 0; coin.vy = 0; state.lastPocketed.push(coin); } }); }); for (let i = 0; i < state.coins.length; i++) { for (let j = i + 1; j < state.coins.length; j++) { const c1 = state.coins[i]; const c2 = state.coins[j]; if (c1.pocketed || c2.pocketed) continue; const dx = c2.x - c1.x; const dy = c2.y - c1.y; const dist = Math.hypot(dx, dy); const minDist = c1.radius + c2.radius; if (dist < minDist && dist > 0) { const overlap = minDist - dist; const nx = dx / dist; const ny = dy / dist; c1.x -= nx * overlap * 0.5; c1.y -= ny * overlap * 0.5; c2.x += nx * overlap * 0.5; c2.y += ny * overlap * 0.5; const dvx = c2.vx - c1.vx; const dvy = c2.vy - c1.vy; const dv = dvx * nx + dvy * ny; if (dv > 0) continue; const impulse = dv; c1.vx += impulse * nx; c1.vy += impulse * ny; c2.vx -= impulse * nx; c2.vy -= impulse * ny; c1.vx *= 0.98; c1.vy *= 0.98; c2.vx *= 0.98; c2.vy *= 0.98; } } } return moving; } function handlePocketedCoins(state) { const playerIndex = state.currentPlayer - 1; const opponentIndex = playerIndex === 0 ? 1 : 0; let validPocket = false; let queenPocketed = false; const striker = state.coins.find(c => c.type === 'striker'); if (striker && striker.pocketed) { striker.pocketed = false; striker.vx = 0; striker.vy = 0; state.foulCommitted = true; } state.lastPocketed.forEach(coin => { if (coin.type === 'queen') { queenPocketed = true; state.queenNeedsCover = true; } else if (coin.type === 'white') { if (state.currentPlayer === 1) { state.scores[0]++; validPocket = true; } else { state.foulCommitted = true; } } else if (coin.type === 'black') { if (state.currentPlayer === 2) { state.scores[1]++; validPocket = true; } else { state.foulCommitted = true; } } }); if (state.queenNeedsCover && validPocket) { state.queenOwner = state.currentPlayer; state.queenNeedsCover = false; state.scores[playerIndex] += 3; } if (state.foulCommitted) { const opponentColor = state.currentPlayer === 1 ? 'black' : 'white'; const returnedCoin = state.coins.find(c => c.type === opponentColor && c.pocketed); if (returnedCoin) { returnedCoin.pocketed = false; returnedCoin.x = BOARD_WIDTH / 2 + (Math.random() - 0.5) * 100; returnedCoin.y = BOARD_HEIGHT / 2 + (Math.random() - 0.5) * 100; state.scores[opponentIndex] = Math.max(0, state.scores[opponentIndex] - 1); } } if (validPocket && !state.foulCommitted) { state.turnAgain = true; } else if (queenPocketed && !validPocket && !state.foulCommitted) { state.turnAgain = true; } else { state.turnAgain = false; } if (state.scores[0] >= 8) { state.gameOver = true; state.winner = 1; } else if (state.scores[1] >= 8) { state.gameOver = true; state.winner = 2; } } function resetStriker(state, playerNum) { const striker = state.coins.find(c => c.type === 'striker'); if (striker) { striker.x = BOARD_WIDTH / 2; striker.y = playerNum === 1 ? STRIKER_LINE_Y1 : STRIKER_LINE_Y2; striker.vx = 0; striker.vy = 0; striker.pocketed = false; } } function startPhysicsLoop(room) { if (room.physicsInterval) { clearInterval(room.physicsInterval); } room.physicsInterval = setInterval(() => { if (!room || !room.state) return; const moving = updatePhysics(room.state); if (room.state.isMoving && !moving) { room.state.isMoving = false; handlePocketedCoins(room.state); if (!room.state.turnAgain && !room.state.gameOver) { room.state.currentPlayer = room.state.currentPlayer === 1 ? 2 : 1; room.state.foulCommitted = false; resetStriker(room.state, room.state.currentPlayer); } else if (room.state.turnAgain) { room.state.turnAgain = false; room.state.foulCommitted = false; resetStriker(room.state, room.state.currentPlayer); } const currentPlayer = room.players.find(p => p.number === room.state.currentPlayer); io.to(room.id).emit('turnChange', { currentPlayer: room.state.currentPlayer, playerName: currentPlayer ? currentPlayer.name : 'Unknown' }); if (room.state.gameOver) { clearInterval(room.physicsInterval); room.physicsInterval = null; const winner = room.players.find(p => p.number === room.state.winner); io.to(room.id).emit('gameOver', { winner: room.state.winner, winnerName: winner ? winner.name : 'Unknown' }); } } else if (moving) { room.state.isMoving = true; } io.to(room.id).emit('gameState', room.state); }, 1000 / 60); } io.on('connection', (socket) => { console.log('Client connected:', socket.id); socket.on('joinRoom', ({ name, roomId: requestedRoom }) => { let roomId = requestedRoom || generateRoomId(); if (!rooms.has(roomId)) { rooms.set(roomId, { id: roomId, players: [], state: createInitialState(), physicsInterval: null, gameStarted: false }); } const room = rooms.get(roomId); if (room.players.length >= 2) { socket.emit('error', 'Room is full'); return; } const playerNumber = room.players.length + 1; room.players.push({ id: socket.id, name: name || `Player ${playerNumber}`, number: playerNumber }); socket.join(roomId); socket.roomId = roomId; socket.playerNumber = playerNumber; socket.emit('roomJoined', { roomId, playerNumber, players: room.players.map(p => ({ name: p.name, number: p.number })) }); socket.to(roomId).emit('playerJoined', { name: name || `Player ${playerNumber}`, playerNumber, players: room.players.map(p => ({ name: p.name, number: p.number })) }); if (room.players.length === 2 && !room.gameStarted) { room.gameStarted = true; io.to(roomId).emit('gameState', room.state); io.to(roomId).emit('turnChange', { currentPlayer: 1, playerName: room.players[0].name }); startPhysicsLoop(room); } }); socket.on('moveStriker', ({ roomId, x }) => { const room = rooms.get(roomId); if (!room || !room.state) return; if (room.state.isMoving) return; const striker = room.state.coins.find(c => c.type === 'striker'); if (striker) { striker.x = Math.max(STRIKER_MIN_X, Math.min(STRIKER_MAX_X, x)); } io.to(roomId).emit('gameState', room.state); }); socket.on('strike', ({ roomId, force, angle }) => { const room = rooms.get(roomId); if (!room || !room.state) return; const player = room.players.find(p => p.id === socket.id); if (!player || player.number !== room.state.currentPlayer) return; if (room.state.isMoving) return; const striker = room.state.coins.find(c => c.type === 'striker'); if (striker) { striker.vx = Math.cos(angle) * force; striker.vy = Math.sin(angle) * force; room.state.isMoving = true; } }); socket.on('resetGame', (roomId) => { const room = rooms.get(roomId); if (!room) return; if (room.physicsInterval) { clearInterval(room.physicsInterval); } room.state = createInitialState(); room.gameStarted = true; startPhysicsLoop(room); io.to(roomId).emit('gameState', room.state); io.to(roomId).emit('gameReset'); io.to(roomId).emit('turnChange', { currentPlayer: 1, playerName: room.players[0] ? room.players[0].name : 'Player 1' }); }); socket.on('disconnect', () => { console.log('Client disconnected:', socket.id); if (socket.roomId) { const room = rooms.get(socket.roomId); if (room) { room.players = room.players.filter(p => p.id !== socket.id); if (room.players.length === 0) { if (room.physicsInterval) { clearInterval(room.physicsInterval); } rooms.delete(socket.roomId); } else { io.to(socket.roomId).emit('playerLeft', { message: 'Opponent left the game' }); if (room.physicsInterval) { clearInterval(room.physicsInterval); room.physicsInterval = null; } room.gameStarted = false; } } } }); }); function generateRoomId() { return Math.random().toString(36).substring(2, 8).toUpperCase(); } const PORT = process.env.PORT || 7860; server.listen(PORT, '0.0.0.0', () => { console.log(`Server running on port ${PORT}`); });