282 lines
9.1 KiB
JavaScript
282 lines
9.1 KiB
JavaScript
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`);
|
|
});
|