# Shattered Void - MMO Strategy Game Development Guide ## Project Overview Shattered Void is a cross-platform text-based MMO strategy game set in a post-collapse galaxy. Players rebuild civilizations from ruins through colony management, fleet construction, exploration, and participation in dynamic galaxy-wide events. ## Development Stack - **Backend**: Node.js with Express framework - **Database**: PostgreSQL (primary) + Redis (caching/sessions) - **Frontend**: React with responsive design - **Real-time**: WebSocket connections - **Authentication**: JWT-based with email verification - **Deployment**: Docker containers with CI/CD pipeline ## Code Quality Standards ### JavaScript/Node.js Standards ```javascript // Use strict ESLint configuration // Prefer async/await over promises // Use descriptive variable names // Include comprehensive error handling // Add JSDoc comments for all functions /** * Calculate resource production for a colony building * @param {Object} building - Building instance with type and level * @param {Object} modifiers - Colony and player modifiers * @returns {number} Production rate per hour */ async function calculateBuildingProduction(building, modifiers) { try { const baseProduction = await getBuildingBaseProduction(building.type, building.level); const totalModifier = 1 + (modifiers.colony || 0) + (modifiers.player || 0); return Math.floor(baseProduction * totalModifier); } catch (error) { logger.error(`Production calculation failed for building ${building.id}:`, error); throw new Error(`Failed to calculate production: ${error.message}`); } } ``` ### Database Best Practices ```sql -- Always use transactions for multi-table operations -- Include proper indexes for performance -- Use foreign key constraints for data integrity -- Add audit columns (created_at, updated_at, created_by) -- Use descriptive column names and comments CREATE TABLE colonies ( id SERIAL PRIMARY KEY, player_id INTEGER NOT NULL REFERENCES players(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, coordinates VARCHAR(20) NOT NULL, -- Format: "A3-91-X" planet_type_id INTEGER REFERENCES planet_types(id), population INTEGER DEFAULT 0 CHECK (population >= 0), morale INTEGER DEFAULT 100 CHECK (morale BETWEEN 0 AND 100), founded_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE(coordinates), INDEX idx_colonies_player_id (player_id), INDEX idx_colonies_coordinates (coordinates) ); ``` ## Debugging Guidelines ### Comprehensive Logging Strategy ```javascript // Use structured logging with context const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', format: winston.format.combine( winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json() ), transports: [ new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), new winston.transports.File({ filename: 'logs/combined.log' }), new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), winston.format.simple() ) }) ] }); // Add request correlation IDs for tracing app.use((req, res, next) => { req.correlationId = require('uuid').v4(); logger.info('Request started', { correlationId: req.correlationId, method: req.method, url: req.url, userAgent: req.get('User-Agent'), ip: req.ip }); next(); }); ``` ### Error Handling Patterns ```javascript // Service layer error handling class ColonyService { async createColony(playerId, colonyData) { const correlationId = require('async_hooks').executionAsyncResource?.correlationId; try { logger.info('Creating colony', { correlationId, playerId, coordinates: colonyData.coordinates }); // Validate input data if (!colonyData.coordinates || !this.isValidCoordinates(colonyData.coordinates)) { throw new ValidationError('Invalid coordinates format'); } // Check if coordinates are already taken const existingColony = await this.getColonyByCoordinates(colonyData.coordinates); if (existingColony) { throw new ConflictError('Coordinates already occupied'); } // Database transaction for atomic operation const colony = await db.transaction(async (trx) => { const [newColony] = await trx('colonies') .insert({ player_id: playerId, name: colonyData.name, coordinates: colonyData.coordinates, planet_type_id: colonyData.planet_type_id }) .returning('*'); // Create initial buildings await this.createInitialBuildings(newColony.id, trx); logger.info('Colony created successfully', { correlationId, colonyId: newColony.id, playerId }); return newColony; }); return colony; } catch (error) { logger.error('Colony creation failed', { correlationId, playerId, coordinates: colonyData.coordinates, error: error.message, stack: error.stack }); // Re-throw with context if (error instanceof ValidationError || error instanceof ConflictError) { throw error; } throw new ServiceError('Failed to create colony', error); } } } // Custom error classes for better error handling class ValidationError extends Error { constructor(message) { super(message); this.name = 'ValidationError'; this.statusCode = 400; } } class ConflictError extends Error { constructor(message) { super(message); this.name = 'ConflictError'; this.statusCode = 409; } } class ServiceError extends Error { constructor(message, originalError) { super(message); this.name = 'ServiceError'; this.statusCode = 500; this.originalError = originalError; } } ``` ### Development Debugging Tools ```javascript // Debug middleware for development if (process.env.NODE_ENV === 'development') { app.use('/debug', require('./middleware/debug')); } // Debug endpoint for inspecting game state app.get('/debug/player/:playerId/state', async (req, res) => { if (process.env.NODE_ENV !== 'development') { return res.status(404).send('Not found'); } try { const playerId = req.params.playerId; const playerState = { player: await Player.findById(playerId), colonies: await Colony.findByPlayerId(playerId), fleets: await Fleet.findByPlayerId(playerId), research: await Research.findByPlayerId(playerId), resources: await Resource.findByPlayerId(playerId) }; res.json({ debug: true, timestamp: new Date().toISOString(), playerState }); } catch (error) { logger.error('Debug endpoint error:', error); res.status(500).json({ error: error.message }); } }); // Performance monitoring const performanceTracker = { trackOperation: (operationName) => { const start = process.hrtime.bigint(); return { end: () => { const duration = Number(process.hrtime.bigint() - start) / 1000000; // Convert to ms logger.info('Operation completed', { operation: operationName, duration: `${duration.toFixed(2)}ms` }); return duration; } }; } }; // Usage in service methods async function processGameTick() { const tracker = performanceTracker.trackOperation('game_tick'); try { await Promise.all([ processResourceProduction(), processFleetMovements(), processResearchCompletion(), processBuildingConstruction() ]); } finally { tracker.end(); } } ``` ## Testing Strategy ### Unit Testing with Jest ```javascript // colony.service.test.js const ColonyService = require('../services/colony.service'); const db = require('../database/connection'); describe('ColonyService', () => { let colonyService; beforeEach(() => { colonyService = new ColonyService(); // Mock database calls jest.clearAllMocks(); }); describe('createColony', () => { it('should create colony with valid data', async () => { const playerId = 1; const colonyData = { name: 'Test Colony', coordinates: 'A3-91-X', planet_type_id: 1 }; // Mock database responses db.transaction = jest.fn().mockImplementation(async (callback) => { const mockTrx = { 'colonies': { insert: jest.fn().mockReturnValue({ returning: jest.fn().mockResolvedValue([{ id: 123, player_id: playerId, ...colonyData }]) }) } }; return callback(mockTrx); }); colonyService.getColonyByCoordinates = jest.fn().mockResolvedValue(null); colonyService.createInitialBuildings = jest.fn().mockResolvedValue(); const result = await colonyService.createColony(playerId, colonyData); expect(result).toHaveProperty('id', 123); expect(result).toHaveProperty('name', 'Test Colony'); expect(colonyService.createInitialBuildings).toHaveBeenCalledWith(123, expect.any(Object)); }); it('should throw ConflictError for occupied coordinates', async () => { const playerId = 1; const colonyData = { name: 'Test Colony', coordinates: 'A3-91-X', planet_type_id: 1 }; colonyService.getColonyByCoordinates = jest.fn().mockResolvedValue({ id: 456, coordinates: 'A3-91-X' }); await expect(colonyService.createColony(playerId, colonyData)) .rejects .toThrow(ConflictError); }); it('should throw ValidationError for invalid coordinates', async () => { const playerId = 1; const colonyData = { name: 'Test Colony', coordinates: 'INVALID', planet_type_id: 1 }; colonyService.isValidCoordinates = jest.fn().mockReturnValue(false); await expect(colonyService.createColony(playerId, colonyData)) .rejects .toThrow(ValidationError); }); }); }); ``` ### Integration Testing ```javascript // integration/colony.integration.test.js const request = require('supertest'); const app = require('../app'); const db = require('../database/connection'); describe('Colony Integration Tests', () => { let authToken; let testPlayer; beforeAll(async () => { // Set up test database await db.migrate.latest(); await db.seed.run(); // Create test player and get auth token const response = await request(app) .post('/api/auth/register') .send({ email: 'test@example.com', password: 'testpassword123', username: 'testplayer' }); authToken = response.body.token; testPlayer = response.body.player; }); afterAll(async () => { await db.destroy(); }); beforeEach(async () => { // Clean up colonies before each test await db('colonies').where('player_id', testPlayer.id).del(); }); it('should create colony via API endpoint', async () => { const colonyData = { name: 'Integration Test Colony', coordinates: 'Z9-99-Z', planet_type_id: 1 }; const response = await request(app) .post('/api/colonies') .set('Authorization', `Bearer ${authToken}`) .send(colonyData) .expect(201); expect(response.body).toHaveProperty('id'); expect(response.body.name).toBe(colonyData.name); expect(response.body.coordinates).toBe(colonyData.coordinates); // Verify in database const dbColony = await db('colonies') .where('id', response.body.id) .first(); expect(dbColony).toHaveProperty('player_id', testPlayer.id); }); it('should return 409 for duplicate coordinates', async () => { const colonyData = { name: 'First Colony', coordinates: 'Z9-99-Y', planet_type_id: 1 }; // Create first colony await request(app) .post('/api/colonies') .set('Authorization', `Bearer ${authToken}`) .send(colonyData) .expect(201); // Try to create second colony with same coordinates const duplicateData = { name: 'Duplicate Colony', coordinates: 'Z9-99-Y', planet_type_id: 1 }; const response = await request(app) .post('/api/colonies') .set('Authorization', `Bearer ${authToken}`) .send(duplicateData) .expect(409); expect(response.body).toHaveProperty('error'); expect(response.body.error).toContain('occupied'); }); }); ``` ## Development Commands ### Essential Scripts ```json { "scripts": { "dev": "nodemon --inspect=0.0.0.0:9229 src/server.js", "start": "node src/server.js", "test": "jest --verbose --coverage", "test:watch": "jest --watch --verbose", "test:integration": "jest --testPathPattern=integration --runInBand", "lint": "eslint src/ --ext .js --fix", "lint:check": "eslint src/ --ext .js", "typecheck": "jsdoc -t node_modules/tsd-jsdoc/dist -r src/ -d temp/ && rm -rf temp/", "db:migrate": "knex migrate:latest", "db:rollback": "knex migrate:rollback", "db:seed": "knex seed:run", "db:reset": "knex migrate:rollback:all && knex migrate:latest && knex seed:run", "docker:build": "docker build -t shattered-void .", "docker:run": "docker-compose up -d", "logs": "tail -f logs/combined.log", "logs:error": "tail -f logs/error.log" } } ``` ### Environment Configuration ```bash # .env.development NODE_ENV=development PORT=3000 LOG_LEVEL=debug # Database DB_HOST=localhost DB_PORT=5432 DB_NAME=shattered_void_dev DB_USER=postgres DB_PASSWORD=password # Redis REDIS_HOST=localhost REDIS_PORT=6379 # JWT JWT_SECRET=your-super-secret-key-change-in-production JWT_EXPIRE=24h # Game Configuration GAME_TICK_INTERVAL=60000 MAX_COLONIES_PER_PLAYER=10 STARTING_RESOURCES_SCRAP=1000 STARTING_RESOURCES_ENERGY=500 # Debug Features ENABLE_DEBUG_ENDPOINTS=true ENABLE_SQL_LOGGING=true ``` ## Performance Monitoring ### Database Query Optimization ```javascript // Monitor slow queries const slowQueryThreshold = 100; // ms db.on('query', (queryData) => { const startTime = Date.now(); queryData.response = queryData.response || {}; const originalCallback = queryData.response.callback; queryData.response.callback = function(...args) { const duration = Date.now() - startTime; if (duration > slowQueryThreshold) { logger.warn('Slow query detected', { query: queryData.sql, bindings: queryData.bindings, duration: `${duration}ms` }); } if (originalCallback) { originalCallback.apply(this, args); } }; }); // Add query performance metrics const queryMetrics = new Map(); function trackQuery(queryName, query) { return async (...args) => { const start = process.hrtime.bigint(); try { const result = await query(...args); const duration = Number(process.hrtime.bigint() - start) / 1000000; // Update metrics const metrics = queryMetrics.get(queryName) || { count: 0, totalTime: 0, maxTime: 0 }; metrics.count++; metrics.totalTime += duration; metrics.maxTime = Math.max(metrics.maxTime, duration); queryMetrics.set(queryName, metrics); return result; } catch (error) { logger.error(`Query failed: ${queryName}`, { error: error.message }); throw error; } }; } ``` ### Memory and Resource Monitoring ```javascript // Memory usage tracking setInterval(() => { const memUsage = process.memoryUsage(); logger.info('Memory usage', { rss: `${Math.round(memUsage.rss / 1024 / 1024)}MB`, heapTotal: `${Math.round(memUsage.heapTotal / 1024 / 1024)}MB`, heapUsed: `${Math.round(memUsage.heapUsed / 1024 / 1024)}MB`, external: `${Math.round(memUsage.external / 1024 / 1024)}MB` }); }, 60000); // Log every minute // Database connection monitoring setInterval(async () => { try { const dbStats = await db.raw('SELECT COUNT(*) as active_connections FROM pg_stat_activity WHERE state = ?', ['active']); logger.info('Database connections', { active: dbStats.rows[0].active_connections }); } catch (error) { logger.error('Failed to get DB connection stats:', error); } }, 30000); ``` ## Security Best Practices ### Input Validation ```javascript const Joi = require('joi'); // Validation schemas const schemas = { createColony: Joi.object({ name: Joi.string().min(3).max(50).required(), coordinates: Joi.string().pattern(/^[A-Z]\d+-\d+-[A-Z]$/).required(), planet_type_id: Joi.number().integer().min(1).required() }), createFleet: Joi.object({ name: Joi.string().min(3).max(50).required(), ships: Joi.array().items( Joi.object({ design_id: Joi.number().integer().min(1).required(), quantity: Joi.number().integer().min(1).max(100).required() }) ).min(1).required() }) }; // Validation middleware function validateRequest(schema) { return (req, res, next) => { const { error, value } = schema.validate(req.body); if (error) { logger.warn('Validation failed', { correlationId: req.correlationId, error: error.details[0].message, body: req.body }); return res.status(400).json({ error: 'Validation failed', details: error.details[0].message }); } req.body = value; // Use sanitized data next(); }; } // Rate limiting const rateLimit = require('express-rate-limit'); const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // Limit each IP to 100 requests per windowMs message: 'Too many requests from this IP', standardHeaders: true, legacyHeaders: false, }); app.use('/api/', apiLimiter); ``` ## File Structure ``` src/ ├── controllers/ # Route handlers ├── services/ # Business logic ├── models/ # Database models ├── middleware/ # Express middleware ├── database/ │ ├── migrations/ # Database schema changes │ ├── seeds/ # Test data │ └── connection.js # Database configuration ├── utils/ # Helper functions ├── validators/ # Input validation schemas ├── tests/ │ ├── unit/ # Unit tests │ ├── integration/ # Integration tests │ └── fixtures/ # Test data ├── config/ # Configuration files └── server.js # Application entry point logs/ # Log files docker/ # Docker configuration docs/ # Documentation scripts/ # Deployment and utility scripts ``` ## Git Workflow - Use feature branches: `feature/colony-management` - Commit messages: `feat: add colony creation API endpoint` - Always run `npm run lint && npm test` before committing - Use conventional commits for automated changelog generation This development guide ensures high code quality, comprehensive debugging capabilities, and maintainable architecture for the Shattered Void MMO project.