Shatteredvoid/CLAUDE.md
MegaProxy 1a60cf55a3 Initial commit: Shattered Void MMO foundation
- Complete PostgreSQL database schema with 21+ tables
- Express.js server with dual authentication (player/admin)
- WebSocket support for real-time features
- Comprehensive middleware (auth, validation, logging, security)
- Game systems: colonies, resources, fleets, research, factions
- Plugin-based combat architecture
- Admin panel foundation
- Production-ready logging and error handling
- Docker support and CI/CD ready
- Complete project structure following CLAUDE.md patterns

🤖 Generated with Claude Code (https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-02 02:13:05 +00:00

21 KiB

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

// 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

-- 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

// 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

// 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

// 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

// 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

// 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

{
  "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

# .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

// 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

// 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

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.