const express = require('express'); const fs = require('fs'); const cors = require('cors'); const app = express(); app.use(cors()); app.use(express.json()); const LEADERBOARD_FILE = './leaderboard.json'; const INACTIVE_DAYS = 30; // Completely remove from JSON after 5 days const LEADERBOARD_VISIBLE_DAYS = 3; // Only show on leaderboard if active within 1 day const BOT_USER_AGENTS = [ 'googlebot', 'bingbot', 'slurp', 'duckduckbot', 'baiduspider', 'yandexbot', 'facebookexternalhit', 'twitterbot', 'bot', 'crawler', 'spider', 'archive', 'wget', 'curl', 'python-requests' ]; // Load leaderboard from file function loadLeaderboard() { try { return JSON.parse(fs.readFileSync(LEADERBOARD_FILE, 'utf8')); } catch (e) { return []; } } // Save leaderboard to file function saveLeaderboard(data) { fs.writeFileSync(LEADERBOARD_FILE, JSON.stringify(data, null, 2)); } // Clean up inactive players (remove from JSON after 30 days) function cleanupInactive(leaderboard) { const thirtyDaysAgo = Date.now() - (INACTIVE_DAYS * 24 * 60 * 60 * 1000); const beforeCount = leaderboard.length; const active = leaderboard.filter(player => player.lastSeen > thirtyDaysAgo); if (active.length < beforeCount) { const timestamp = new Date().toLocaleString(); console.log(`${timestamp} Permanently removed ${beforeCount - active.length} players (>30 days inactive)`); } return active; } // Filter players visible on leaderboard (active within 5 days) function getVisiblePlayers(leaderboard) { const fiveDaysAgo = Date.now() - (LEADERBOARD_VISIBLE_DAYS * 24 * 60 * 60 * 1000); const visible = leaderboard.filter(player => player.lastSeen > fiveDaysAgo && !player.id.startsWith('admin_') ); return visible; } // Submit checkpoint app.post('/api/checkpoint', (req, res) => { const userAgent = (req.headers['user-agent'] || '').toLowerCase(); // Reject known bots if (BOT_USER_AGENTS.some(bot => userAgent.includes(bot))) { return res.status(403).json({ error: 'Bot traffic not allowed' }); } const { playerId, gameState } = req.body; // Ignore players who haven't consumed any mass if (gameState.totalMassConsumedEver === 0) { return res.json({ success: true }); // Accept but don't save } // Only save if they've upgraded at least once const totalLevels = (gameState.asteroidUpgradeLevel || 0) + (gameState.cometUpgradeLevel || 0) + (gameState.planetUpgradeLevel || 0) + (gameState.giantUpgradeLevel || 0); if (totalLevels === 0 && gameState.totalMassConsumedEver < 1e6) { return res.json({ success: true }); // Accept but don't save } // Basic validation if (!playerId || !gameState) { return res.status(400).json({ error: 'Invalid data' }); } let leaderboard = loadLeaderboard(); // Clean up players inactive for 30+ days leaderboard = cleanupInactive(leaderboard); const now = Date.now(); // Find or create player let player = leaderboard.find(p => p.id === playerId); if (player) { // Update existing player - store full game state player.gameState = gameState; player.mass = gameState.blackHoleTotalMass || 0; player.level = (gameState.asteroidUpgradeLevel || 0) + (gameState.cometUpgradeLevel || 0) + (gameState.planetUpgradeLevel || 0) + (gameState.giantUpgradeLevel || 0); player.lastSeen = now; player.holeAge = now - player.firstSeen; } else { // New player const timestamp = new Date().toLocaleString(); const newPlayer = { id: playerId, gameState: gameState, mass: gameState.blackHoleTotalMass || 0, level: (gameState.asteroidUpgradeLevel || 0) + (gameState.cometUpgradeLevel || 0) + (gameState.planetUpgradeLevel || 0) + (gameState.giantUpgradeLevel || 0), firstSeen: now, lastSeen: now, holeAge: 0 }; console.log(`${timestamp} Created new player: `, newPlayer.id); leaderboard.push(newPlayer); } // Sort by mass and keep top 100 leaderboard.sort((a, b) => b.mass - a.mass); leaderboard = leaderboard.slice(0, 100); saveLeaderboard(leaderboard); res.json({ success: true }); }); // Get leaderboard app.get('/api/leaderboard', (req, res) => { const type = req.query.type || 'mass'; const requesterId = req.query.playerId; // Get player ID from query parameter let leaderboard = loadLeaderboard(); // Clean up players inactive for 30+ days leaderboard = cleanupInactive(leaderboard); // Update hole ages and online status const now = Date.now(); const fifteenMinutes = 15 * 60 * 1000; leaderboard.forEach(player => { player.holeAge = now - player.firstSeen; player.isOnline = (now - player.lastSeen) < fifteenMinutes; }); // Filter to only show players active within 5 days let visiblePlayers = getVisiblePlayers(leaderboard); // Sort by requested type if (type === 'age') { visiblePlayers.sort((a, b) => b.holeAge - a.holeAge); } else if (type === 'time') { visiblePlayers.sort((a, b) => b.playTime - a.playTime); } else if (type === 'level') { visiblePlayers.sort((a, b) => b.level - a.level); } else { visiblePlayers.sort((a, b) => b.mass - a.mass); } // Log who opened the leaderboard if (requesterId) { const requester = leaderboard.find(p => p.id === requesterId); const requesterName = requester ? requester.name : 'Unknown'; const timestamp = new Date().toLocaleString(); console.log(`${timestamp} Score requested by: ${requesterName} (${requesterId.slice(-5)})`); } // Save the full leaderboard (including hidden players) saveLeaderboard(leaderboard); // Return only visible players (top 50) res.json(visiblePlayers.slice(0, 50)); }); // Get player info (works even if not visible on leaderboard) app.get('/api/player/:playerId', (req, res) => { const playerId = req.params.playerId; let leaderboard = loadLeaderboard(); const player = leaderboard.find(p => p.id === playerId); if (!player) { return res.status(404).json({ error: 'Player not found' }); } // Update hole age player.holeAge = Date.now() - player.firstSeen; // Calculate days since last seen player.daysSinceLastSeen = Math.floor((Date.now() - player.lastSeen) / (24 * 60 * 60 * 1000)); // Check if visible on leaderboard const fiveDaysAgo = Date.now() - (LEADERBOARD_VISIBLE_DAYS * 24 * 60 * 60 * 1000); player.visibleOnLeaderboard = player.lastSeen > fiveDaysAgo; res.json(player); }); // Get player's full game state app.get('/api/player/:playerId/state', (req, res) => { const playerId = req.params.playerId; let leaderboard = loadLeaderboard(); const player = leaderboard.find(p => p.id === playerId); if (!player || !player.gameState) { return res.status(404).json({ error: 'Player not found' }); } res.json(player.gameState); }); // Manual cleanup endpoint (optional - for maintenance) app.post('/api/cleanup', (req, res) => { let leaderboard = loadLeaderboard(); const beforeCount = leaderboard.length; leaderboard = cleanupInactive(leaderboard); saveLeaderboard(leaderboard); res.json({ removed: beforeCount - leaderboard.length, remaining: leaderboard.length }); }); // Stats endpoint (optional - see how many are hidden vs visible) app.get('/api/stats', (req, res) => { let leaderboard = loadLeaderboard(); const visiblePlayers = getVisiblePlayers(leaderboard); res.json({ totalPlayers: leaderboard.length, visiblePlayers: visiblePlayers.length, hiddenPlayers: leaderboard.length - visiblePlayers.length }); }); // Cleanup bot accounts function function cleanupBotAccounts() { let leaderboard = loadLeaderboard(); const beforeCount = leaderboard.length; // Remove accounts with zero progress leaderboard = leaderboard.filter(player => { const hasProgress = player.gameState && (player.gameState.totalMassConsumedEver > 0 || player.level > 0); return hasProgress; }); if (leaderboard.length < beforeCount) { console.log(`Cleaned up ${beforeCount - leaderboard.length} bot/inactive accounts`); saveLeaderboard(leaderboard); } } // Run cleanup daily (24 hours) setInterval(cleanupBotAccounts, 24 * 60 * 60 * 1000); // Run cleanup on startup too cleanupBotAccounts(); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { const timestamp = new Date().toLocaleString(); console.log(`${timestamp} Server running on port ${PORT}`); console.log(`${timestamp} Players hidden from leaderboard after ${LEADERBOARD_VISIBLE_DAYS} days inactive`); console.log(`${timestamp} Players permanently removed after ${INACTIVE_DAYS} days inactive`); });