const express = require('express'); const http = require('http'); const { Server } = require('socket.io'); const path = require('path'); const zlib = require('zlib'); const app = express(); const server = http.createServer(app); // Professional Socket.IO configuration const io = new Server(server, { cors: { origin: "*", methods: ["GET", "POST"] }, // Optimized for slow connections pingTimeout: 120000, pingInterval: 10000, upgradeTimeout: 30000, transports: ['websocket', 'polling'], allowUpgrades: true, perMessageDeflate: { threshold: 256, zlibDeflateOptions: { level: 6 }, zlibInflateOptions: { chunkSize: 10 * 1024 } }, httpCompression: true, maxHttpBufferSize: 1e6 }); app.use(express.static(path.join(__dirname, 'public'))); // Game constants const TILE_SIZE = 10; const GRID_WIDTH = 13; const BASE_TICK_RATE = 15; // Lower for bandwidth const HOP_DURATION = 120; // Game state const players = new Map(); const gameState = { lanes: new Map(), furthestLaneIndex: 14, version: 0 // State version for delta sync }; // Connection stats const connectionStats = new Map(); // ============= LANE GENERATION ============= function generateLaneData(index, prevLane) { let type = 'grass'; let runLength = 1; if (index <= 4) { type = 'grass'; } else if (prevLane) { const r = Math.random(); if (prevLane.type === 'grass') { if (r < 0.6) type = 'road'; else if (r < 0.9) type = 'water'; else type = 'grass'; } else if (prevLane.type === 'road') { if (prevLane.runLength < 4 && r < 0.7) { type = 'road'; runLength = prevLane.runLength + 1; } else { type = 'grass'; } } else if (prevLane.type === 'water') { if (prevLane.runLength < 2 && r < 0.6) { type = 'water'; runLength = prevLane.runLength + 1; } else { type = 'grass'; } } } const staticObstacles = []; if (type === 'grass') { const treeCount = Math.floor(Math.random() * 3); for (let i = 0; i < treeCount; i++) { let gx = Math.floor(Math.random() * GRID_WIDTH) - Math.floor(GRID_WIDTH / 2); if (index < 5 && gx === 0) continue; if (!staticObstacles.includes(gx)) { staticObstacles.push(gx); } } } const lane = { i: index, // Short keys for bandwidth t: type.charAt(0), // 'g', 'r', 'w' rl: runLength, so: staticObstacles, sp: Math.round((Math.random() * 0.04 + 0.02) * TILE_SIZE * 100) / 100, d: Math.random() > 0.5 ? 1 : -1, obs: [], timer: 0, interval: Math.random() * 100 + 150, obsId: 0 }; // Pre-populate obstacles if (type === 'road' || type === 'water') { const count = Math.ceil(Math.random() * 2); for (let i = 0; i < count; i++) { let randX = (Math.random() * 120) - 60; if (Math.abs(randX) < 15) randX += (randX > 0 ? 20 : -20); const obsWidth = type === 'water' ? (Math.random() > 0.5 ? 40 : 60) : 12; lane.obs.push({ id: lane.obsId++, x: Math.round(randX * 10) / 10, l: type === 'water' ? 1 : 0, // isLog w: obsWidth }); } } return lane; } function initializeLanes() { gameState.lanes.clear(); for (let i = -4; i < 15; i++) { const prevLane = gameState.lanes.get(i - 1); gameState.lanes.set(i, generateLaneData(i, prevLane)); } gameState.furthestLaneIndex = 14; } initializeLanes(); // ============= COLLISION DETECTION ============= function checkCollisionAtPosition(gridX, gridZ, worldX = null) { const lane = gameState.lanes.get(gridZ); if (!lane) return { hit: false }; const px = worldX !== null ? worldX : gridX * TILE_SIZE; if (Math.abs(px) > (GRID_WIDTH * TILE_SIZE / 2 + 10)) { return { hit: true, type: 'bounds' }; } if (lane.t === 'g') { if (lane.so.includes(gridX)) { return { hit: true, type: 'tree', blocked: true }; } return { hit: false }; } if (lane.t === 'r') { for (const obs of lane.obs) { const left = obs.x - obs.w / 2; const right = obs.x + obs.w / 2; if (px + 3 > left && px - 3 < right) { return { hit: true, type: 'car' }; } } return { hit: false }; } if (lane.t === 'w') { for (const obs of lane.obs) { if (Math.abs(px - obs.x) < (obs.w / 2) + 4) { return { hit: false, onLog: true, logId: obs.id }; } } return { hit: true, type: 'water' }; } return { hit: false }; } function validateMove(player, dx, dz) { if (!player.alive) return { valid: false, reason: 'dead' }; if (dz < 0) return { valid: false, reason: 'backward' }; const targetX = player.gx + dx; const targetZ = player.gz + dz; if (Math.abs(targetX) > Math.floor(GRID_WIDTH / 2)) { return { valid: false, reason: 'bounds' }; } const lane = gameState.lanes.get(targetZ); if (lane && lane.t === 'g' && lane.so.includes(targetX)) { return { valid: false, reason: 'tree' }; } return { valid: true, tx: targetX, tz: targetZ }; } // ============= LEADERBOARD ============= function getLeaderboard() { return Array.from(players.values()) .filter(p => p.alive || p.score > 0) .sort((a, b) => b.score - a.score) .slice(0, 3) .map((p, i) => ({ r: i + 1, n: p.name, s: p.score })); } // Compact player data for network function compactPlayer(p) { return { id: p.id, n: p.name, gx: p.gx, gz: p.gz, wx: Math.round(p.wx * 10) / 10, wz: Math.round(p.wz * 10) / 10, r: Math.round(p.rot * 100) / 100, s: p.score, a: p.alive ? 1 : 0, h: p.hopping ? 1 : 0, ht: p.hopTime }; } // Compact lane data function compactLane(lane) { return { i: lane.i, t: lane.t, rl: lane.rl, so: lane.so, sp: lane.sp, d: lane.d, obs: lane.obs.map(o => ({ id: o.id, x: Math.round(o.x * 10) / 10, l: o.l, w: o.w })) }; } // ============= SOCKET HANDLING ============= io.on('connection', (socket) => { console.log(`[${new Date().toISOString()}] Connection: ${socket.id}`); // Initialize connection stats connectionStats.set(socket.id, { connected: Date.now(), lastPing: Date.now(), ping: 0, quality: 'good' }); // Send immediate acknowledgment socket.emit('ack', { id: socket.id, t: Date.now(), v: gameState.version }); socket.on('join', (data) => { try { const name = (data.name || 'Anon').substring(0, 12).replace(/[<>\"\'&]/g, ''); const player = { id: socket.id, name: name, gx: 0, gz: 0, wx: 0, wz: 0, score: 0, alive: true, hopping: false, hopTime: 0, hopStart: { x: 0, z: 0 }, hopTarget: { x: 0, z: 0 }, rot: 0, logId: null, lastMove: 0, moveSeq: 0 }; players.set(socket.id, player); // Send minimal initial state const lanesArr = []; gameState.lanes.forEach((lane) => { lanesArr.push(compactLane(lane)); }); const otherPlayers = []; players.forEach((p, id) => { if (id !== socket.id) { otherPlayers.push(compactPlayer(p)); } }); socket.emit('init', { p: compactPlayer(player), ps: otherPlayers, ls: lanesArr, t: Date.now(), lb: getLeaderboard() }); // Notify others socket.broadcast.emit('pj', compactPlayer(player)); console.log(`[${new Date().toISOString()}] Joined: ${name} (${socket.id})`); } catch (err) { console.error('Join error:', err); socket.emit('err', { m: 'Join failed' }); } }); socket.on('m', (data) => { // Move const player = players.get(socket.id); if (!player) return; const now = Date.now(); // Rate limit: 70ms minimum if (now - player.lastMove < 70) { socket.emit('mr', { s: data.s, r: 'fast' }); return; } if (player.hopping) { socket.emit('mr', { s: data.s, r: 'hop' }); return; } if (!player.alive) { socket.emit('mr', { s: data.s, r: 'dead' }); return; } const { dx, dz, s } = data; const v = validateMove(player, dx, dz); if (!v.valid) { socket.emit('mr', { s, r: v.reason }); return; } // Execute move player.lastMove = now; player.hopping = true; player.hopTime = now; player.hopStart = { x: player.wx, z: player.wz }; player.hopTarget = { x: v.tx * TILE_SIZE, z: v.tz * TILE_SIZE }; player.gx = v.tx; player.gz = v.tz; player.wx = v.tx * TILE_SIZE; player.wz = v.tz * TILE_SIZE; player.logId = null; player.moveSeq = s; // Rotation if (dx === 1) player.rot = -Math.PI / 2; else if (dx === -1) player.rot = Math.PI / 2; else if (dz === 1) player.rot = 0; else if (dz === -1) player.rot = Math.PI; // Score if (v.tz > player.score) { player.score = v.tz; io.emit('lb', getLeaderboard()); } // Generate new lanes const spawnZ = player.score + 12; while (spawnZ > gameState.furthestLaneIndex) { gameState.furthestLaneIndex++; const prevLane = gameState.lanes.get(gameState.furthestLaneIndex - 1); const newLane = generateLaneData(gameState.furthestLaneIndex, prevLane); gameState.lanes.set(gameState.furthestLaneIndex, newLane); io.emit('nl', compactLane(newLane)); } // Confirm socket.emit('mc', { s, gx: player.gx, gz: player.gz, sc: player.score, t: now }); // Broadcast socket.broadcast.emit('pm', { id: player.id, gx: player.gx, gz: player.gz, wx: player.wx, wz: player.wz, r: player.rot, t: now, hs: player.hopStart, ht: player.hopTarget }); }); socket.on('p', (ct) => { // Ping const stats = connectionStats.get(socket.id); if (stats) { stats.lastPing = Date.now(); } socket.emit('po', { c: ct, s: Date.now() }); }); socket.on('rs', () => { // Respawn const player = players.get(socket.id); if (!player) return; player.gx = 0; player.gz = 0; player.wx = 0; player.wz = 0; player.score = 0; player.alive = true; player.hopping = false; player.logId = null; player.rot = 0; socket.emit('rsd', compactPlayer(player)); socket.broadcast.emit('prs', { id: player.id }); io.emit('lb', getLeaderboard()); }); socket.on('hb', () => { // Heartbeat socket.emit('hba'); }); socket.on('disconnect', (reason) => { console.log(`[${new Date().toISOString()}] Disconnect: ${socket.id} (${reason})`); players.delete(socket.id); connectionStats.delete(socket.id); io.emit('pl', socket.id); io.emit('lb', getLeaderboard()); }); socket.on('error', (err) => { console.error(`Socket error ${socket.id}:`, err); }); }); // ============= GAME LOOP ============= let lastTick = Date.now(); function gameTick() { const now = Date.now(); const dt = now - lastTick; lastTick = now; // Update obstacles gameState.lanes.forEach((lane) => { if (lane.t === 'r' || lane.t === 'w') { lane.timer += dt; if (lane.timer > lane.interval) { const startX = -lane.d * (GRID_WIDTH * TILE_SIZE / 2 + 60); const obsWidth = lane.t === 'w' ? (Math.random() > 0.5 ? 40 : 60) : 12; lane.obs.push({ id: lane.obsId++, x: startX, l: lane.t === 'w' ? 1 : 0, w: obsWidth }); lane.timer = 0; lane.interval = Math.random() * 100 + 200; } // Move obstacles for (let i = lane.obs.length - 1; i >= 0; i--) { const obs = lane.obs[i]; obs.x += lane.sp * lane.d * (dt / 16.67); if (Math.abs(obs.x) > 400) { lane.obs.splice(i, 1); } } } }); // Update players players.forEach((player) => { if (!player.alive) return; // Complete hop if (player.hopping && now - player.hopTime >= HOP_DURATION) { player.hopping = false; const col = checkCollisionAtPosition(player.gx, player.gz, player.wx); if (col.hit) { player.alive = false; io.emit('pd', { id: player.id, type: col.type }); io.emit('lb', getLeaderboard()); } else if (col.onLog) { player.logId = col.logId; } } // Check collision during hop if (player.hopping) { const progress = (now - player.hopTime) / HOP_DURATION; const cx = player.hopStart.x + (player.hopTarget.x - player.hopStart.x) * progress; const cz = player.hopStart.z + (player.hopTarget.z - player.hopStart.z) * progress; const cgz = Math.round(cz / TILE_SIZE); const lane = gameState.lanes.get(cgz); if (lane && lane.t === 'r') { for (const obs of lane.obs) { if (Math.abs(cx - obs.x) < (obs.w / 2 + 3)) { player.alive = false; player.hopping = false; io.emit('pd', { id: player.id, type: 'car' }); io.emit('lb', getLeaderboard()); break; } } } } // Log riding if (!player.hopping && player.logId !== null) { const lane = gameState.lanes.get(player.gz); if (lane) { const log = lane.obs.find(o => o.id === player.logId); if (log) { player.wx = log.x; player.gx = Math.round(player.wx / TILE_SIZE); if (Math.abs(player.wx) > (GRID_WIDTH * TILE_SIZE / 2 + 10)) { player.alive = false; io.emit('pd', { id: player.id, type: 'bounds' }); io.emit('lb', getLeaderboard()); } } else { player.logId = null; if (lane.t === 'w') { player.alive = false; io.emit('pd', { id: player.id, type: 'water' }); io.emit('lb', getLeaderboard()); } } } } }); // Broadcast state (minimal) const playersData = []; players.forEach(p => playersData.push(compactPlayer(p))); const obsData = {}; gameState.lanes.forEach((lane, key) => { if (lane.t !== 'g' && lane.obs.length > 0) { obsData[key] = lane.obs.map(o => ({ id: o.id, x: Math.round(o.x * 10) / 10 })); } }); io.emit('gs', { ps: playersData, obs: obsData, t: now }); } setInterval(gameTick, 1000 / BASE_TICK_RATE); // Cleanup old lanes setInterval(() => { let minZ = Infinity; players.forEach(p => { if (p.gz < minZ) minZ = p.gz; }); if (minZ === Infinity) minZ = 0; gameState.lanes.forEach((lane, key) => { if (key < minZ - 15) { gameState.lanes.delete(key); } }); }, 10000); // Health check endpoint app.get('/health', (req, res) => { res.json({ status: 'ok', players: players.size, uptime: process.uptime() }); }); const PORT = process.env.PORT || 7860; server.listen(PORT, '0.0.0.0', () => { console.log(`Server running on port ${PORT}`); });