Carrom / server.js
srinetwork's picture
Create server.js
2a73c39 verified
Raw History Blame Contribute Delete
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}`);
});