Download server.js from wpms/Carrom: direct link, hf CLI and curl.
- Browser
- Download file 14.9 kB
-
https://huggingface.co/spaces/wpms/Carrom/resolve/main/server.js
- Command line
-
hf download hf://spaces/wpms/Carrom/server.js
-
curl -L -o server.js https://huggingface.co/spaces/wpms/Carrom/resolve/main/server.js
14.9 kB
| 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}`); | |
| }); | |