Spacey-Backend / server.js
abcd118q's picture
Upload 5 files
3bfb0ad verified
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
const path = require('path');
const physics = require('./physics');
const PORT = process.env.PORT || 7860;
const CLIENT_ORIGIN = process.env.CLIENT_ORIGIN || 'http://localhost:3000';
const TICK_RATE = 10;
const TICK_INTERVAL = 1000 / TICK_RATE;
const OBSERVATION_PHASE_DURATION = 15000;
const GUESS_PHASE_DURATION = 10000;
const RESULTS_PHASE_DURATION = 5000;
const app = express();
app.use(cors({ origin: CLIENT_ORIGIN }));
app.use(express.json());
app.get('/health', (req, res) => {
res.json({ status: 'ok', satellites: Object.keys(physics.tleCache).length });
});
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: CLIENT_ORIGIN,
methods: ['GET', 'POST'],
},
});
const games = new Map();
class Game {
constructor(roomId) {
this.roomId = roomId;
this.players = [];
this.state = 'waiting';
this.currentSatellite = null;
this.currentSatelliteName = null;
this.phase = null;
this.phaseDeadline = null;
this.phaseTimer = null;
this.tickInterval = null;
this.guesses = new Map();
this.round = 0;
this.scores = new Map();
}
addPlayer(socketId, playerName) {
this.players.push({ socketId, name: playerName, ready: false });
this.scores.set(socketId, 0);
}
removePlayer(socketId) {
this.players = this.players.filter(p => p.socketId !== socketId);
this.scores.delete(socketId);
if (this.players.length === 0) {
this.stop();
games.delete(this.roomId);
}
}
setReady(socketId) {
const player = this.players.find(p => p.socketId === socketId);
if (player) player.ready = true;
}
allReady() {
return this.players.length >= 2 && this.players.every(p => p.ready);
}
async start() {
this.state = 'playing';
this.round = 0;
await physics.ensureTleFresh();
this.startRound();
}
async startRound() {
this.round++;
this.guesses = new Map();
this.phase = 'observation';
const satState = await physics.getRandomSatelliteState(new Date());
if (!satState) {
io.to(this.roomId).emit('GAME_ERROR', { message: 'No satellite data available' });
return;
}
this.currentSatellite = satState;
this.currentSatelliteName = satState.name;
io.to(this.roomId).emit('ROUND_START', {
round: this.round,
phase: 'observation',
duration: OBSERVATION_PHASE_DURATION,
satelliteName: satState.name,
});
this.phaseDeadline = Date.now() + OBSERVATION_PHASE_DURATION;
this.clearPhaseTimers();
if (this.tickInterval) clearInterval(this.tickInterval);
this.tickInterval = setInterval(() => this.tick(), TICK_INTERVAL);
this.phaseTimer = setTimeout(() => this.startGuessPhase(), OBSERVATION_PHASE_DURATION);
}
startGuessPhase() {
this.phase = 'guess';
this.phaseDeadline = Date.now() + GUESS_PHASE_DURATION;
if (this.tickInterval) {
clearInterval(this.tickInterval);
this.tickInterval = null;
}
io.to(this.roomId).emit('PHASE_CHANGE', {
phase: 'guess',
duration: GUESS_PHASE_DURATION,
});
this.phaseTimer = setTimeout(() => this.endRound(), GUESS_PHASE_DURATION);
}
endRound() {
this.phase = 'results';
if (this.tickInterval) {
clearInterval(this.tickInterval);
this.tickInterval = null;
}
const results = [];
for (const [socketId, guess] of this.guesses) {
const distance = physics.haversineDistance(
this.currentSatellite.lat, this.currentSatellite.lng,
guess.lat, guess.lng
);
const score = physics.scoreFromDistance(distance);
this.scores.set(socketId, (this.scores.get(socketId) || 0) + score);
const player = this.players.find(p => p.socketId === socketId);
results.push({
playerName: player ? player.name : 'Unknown',
guessLat: guess.lat,
guessLng: guess.lng,
distance: Math.round(distance),
score,
totalScore: this.scores.get(socketId),
});
}
const sorted = [...results].sort((a, b) => b.score - a.score);
io.to(this.roomId).emit('ROUND_RESULTS', {
round: this.round,
satelliteName: this.currentSatelliteName,
correctLat: this.currentSatellite.lat,
correctLng: this.currentSatellite.lng,
results: sorted,
scores: Object.fromEntries(this.scores),
});
this.phaseTimer = setTimeout(() => {
if (this.round >= 5) {
this.endGame();
} else {
this.startRound();
}
}, RESULTS_PHASE_DURATION);
}
endGame() {
this.state = 'finished';
this.clearPhaseTimers();
if (this.tickInterval) {
clearInterval(this.tickInterval);
this.tickInterval = null;
}
const leaderboard = this.players
.map(p => ({ name: p.name, score: this.scores.get(p.socketId) || 0 }))
.sort((a, b) => b.score - a.score);
io.to(this.roomId).emit('GAME_OVER', { leaderboard });
}
tick() {
if (this.phase !== 'observation' || !this.currentSatellite) return;
const now = new Date();
const updated = physics.getSatelliteState(this.currentSatelliteName, now);
if (updated) {
this.currentSatellite = updated;
io.to(this.roomId).emit('SAT_UPDATE', {
lat: updated.lat,
lng: updated.lng,
alt: updated.alt,
footprintRadius: updated.footprintRadius,
});
}
}
submitGuess(socketId, lat, lng) {
if (this.phase !== 'guess') return;
this.guesses.set(socketId, { lat, lng });
io.to(socketId).emit('GUESS_CONFIRMED', { lat, lng });
}
clearPhaseTimers() {
if (this.phaseTimer) {
clearTimeout(this.phaseTimer);
this.phaseTimer = null;
}
}
stop() {
this.clearPhaseTimers();
if (this.tickInterval) {
clearInterval(this.tickInterval);
this.tickInterval = null;
}
}
}
function getRoomIdFromSocket(socket) {
const rooms = [...socket.rooms].filter(r => r !== socket.id);
return rooms.length > 0 ? rooms[0] : null;
}
io.on('connection', (socket) => {
console.log(`Player connected: ${socket.id}`);
socket.on('CREATE_ROOM', (data, callback) => {
const roomId = Math.random().toString(36).substring(2, 8).toUpperCase();
const playerName = (data && data.playerName) || `Player ${socket.id.substring(0, 4)}`;
const game = new Game(roomId);
game.addPlayer(socket.id, playerName);
games.set(roomId, game);
socket.join(roomId);
socket.emit('ROOM_CREATED', { roomId, playerName });
if (callback) callback({ success: true, roomId });
});
socket.on('JOIN_ROOM', (data, callback) => {
const { roomId, playerName } = data || {};
if (!roomId) {
if (callback) callback({ success: false, error: 'Room ID required' });
return;
}
const game = games.get(roomId);
if (!game) {
if (callback) callback({ success: false, error: 'Room not found' });
return;
}
if (game.players.length >= 2) {
if (callback) callback({ success: false, error: 'Room is full' });
return;
}
if (game.state !== 'waiting') {
if (callback) callback({ success: false, error: 'Game already in progress' });
return;
}
const name = playerName || `Player ${socket.id.substring(0, 4)}`;
game.addPlayer(socket.id, name);
socket.join(roomId);
io.to(roomId).emit('PLAYER_JOINED', {
players: game.players.map(p => ({ name: p.name, ready: p.ready })),
});
if (callback) callback({ success: true });
});
socket.on('PLAYER_READY', () => {
const roomId = getRoomIdFromSocket(socket);
if (!roomId) return;
const game = games.get(roomId);
if (!game) return;
game.setReady(socket.id);
io.to(roomId).emit('PLAYER_READY_UPDATE', {
players: game.players.map(p => ({ name: p.name, ready: p.ready })),
});
if (game.allReady()) {
io.to(roomId).emit('GAME_STARTING');
game.start();
}
});
socket.on('SUBMIT_GUESS', (data) => {
const { lat, lng } = data || {};
if (lat == null || lng == null) return;
const roomId = getRoomIdFromSocket(socket);
if (!roomId) return;
const game = games.get(roomId);
if (!game) return;
game.submitGuess(socket.id, lat, lng);
});
socket.on('disconnect', () => {
console.log(`Player disconnected: ${socket.id}`);
for (const [roomId, game] of games) {
if (game.players.some(p => p.socketId === socket.id)) {
socket.to(roomId).emit('PLAYER_DISCONNECTED', {
playerName: game.players.find(p => p.socketId === socket.id)?.name,
});
game.removePlayer(socket.id);
break;
}
}
});
});
async function init() {
console.log('Fetching initial TLE data...');
await physics.fetchTleData();
console.log(`Loaded ${Object.keys(physics.tleCache).length} satellites`);
setInterval(async () => {
console.log('Refreshing TLE data...');
await physics.fetchTleData();
}, 60 * 60 * 1000);
server.listen(PORT, '0.0.0.0', () => {
console.log(`Orbit Guesser server running on port ${PORT}`);
});
}
init();