- 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>
21 KiB
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 testbefore 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.