burnrate 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +507 -0
  3. package/dist/cli/format.d.ts +13 -0
  4. package/dist/cli/format.js +319 -0
  5. package/dist/cli/index.d.ts +7 -0
  6. package/dist/cli/index.js +1121 -0
  7. package/dist/cli/setup.d.ts +8 -0
  8. package/dist/cli/setup.js +143 -0
  9. package/dist/core/async-engine.d.ts +411 -0
  10. package/dist/core/async-engine.js +2274 -0
  11. package/dist/core/async-worldgen.d.ts +19 -0
  12. package/dist/core/async-worldgen.js +221 -0
  13. package/dist/core/engine.d.ts +154 -0
  14. package/dist/core/engine.js +1104 -0
  15. package/dist/core/pathfinding.d.ts +38 -0
  16. package/dist/core/pathfinding.js +146 -0
  17. package/dist/core/types.d.ts +489 -0
  18. package/dist/core/types.js +359 -0
  19. package/dist/core/worldgen.d.ts +22 -0
  20. package/dist/core/worldgen.js +292 -0
  21. package/dist/db/database.d.ts +83 -0
  22. package/dist/db/database.js +829 -0
  23. package/dist/db/turso-database.d.ts +177 -0
  24. package/dist/db/turso-database.js +1586 -0
  25. package/dist/mcp/server.d.ts +7 -0
  26. package/dist/mcp/server.js +1877 -0
  27. package/dist/server/api.d.ts +8 -0
  28. package/dist/server/api.js +1234 -0
  29. package/dist/server/async-tick-server.d.ts +5 -0
  30. package/dist/server/async-tick-server.js +63 -0
  31. package/dist/server/errors.d.ts +78 -0
  32. package/dist/server/errors.js +156 -0
  33. package/dist/server/rate-limit.d.ts +22 -0
  34. package/dist/server/rate-limit.js +134 -0
  35. package/dist/server/tick-server.d.ts +9 -0
  36. package/dist/server/tick-server.js +114 -0
  37. package/dist/server/validation.d.ts +194 -0
  38. package/dist/server/validation.js +114 -0
  39. package/package.json +65 -0
@@ -0,0 +1,5 @@
1
+ /**
2
+ * BURNRATE Async Tick Server
3
+ * Processes game ticks at regular intervals using TursoDatabase
4
+ */
5
+ export {};
@@ -0,0 +1,63 @@
1
+ /**
2
+ * BURNRATE Async Tick Server
3
+ * Processes game ticks at regular intervals using TursoDatabase
4
+ */
5
+ import { TursoDatabase } from '../db/turso-database.js';
6
+ import { AsyncGameEngine } from '../core/async-engine.js';
7
+ const TICK_INTERVAL = parseInt(process.env.BURNRATE_TICK_INTERVAL || '600000'); // 10 minutes
8
+ async function main() {
9
+ console.log(`
10
+ ╔══════════════════════════════════════════════════════════════╗
11
+ ║ BURNRATE TICK SERVER ║
12
+ ║ The front doesn't feed itself. ║
13
+ ╠══════════════════════════════════════════════════════════════╣
14
+ ║ Tick interval: ${(TICK_INTERVAL / 1000).toString().padEnd(6)}seconds ║
15
+ ║ Press Ctrl+C to stop ║
16
+ ╚══════════════════════════════════════════════════════════════╝
17
+ `);
18
+ const db = new TursoDatabase();
19
+ await db.initialize();
20
+ const engine = new AsyncGameEngine(db);
21
+ const currentTick = await db.getCurrentTick();
22
+ console.log(`[TICK SERVER] Starting at tick ${currentTick}`);
23
+ async function processTick() {
24
+ const startTime = Date.now();
25
+ try {
26
+ const result = await engine.processTick();
27
+ const duration = Date.now() - startTime;
28
+ console.log(`[TICK ${result.tick}] Processed in ${duration}ms, ${result.events.length} events`);
29
+ // Log significant events
30
+ for (const event of result.events) {
31
+ if (event.type === 'zone_captured') {
32
+ console.log(` → Zone ${event.data.zoneName} captured by ${event.data.newOwner || 'neutral'}`);
33
+ }
34
+ else if (event.type === 'zone_state_changed') {
35
+ console.log(` → Zone ${event.data.zoneName}: ${event.data.previousState} → ${event.data.newState}`);
36
+ }
37
+ else if (event.type === 'combat_resolved') {
38
+ console.log(` → Combat at ${event.data.location}: ${event.data.outcome}`);
39
+ }
40
+ else if (event.type === 'shipment_intercepted') {
41
+ console.log(` → Shipment intercepted: ${event.data.outcome}`);
42
+ }
43
+ }
44
+ }
45
+ catch (error) {
46
+ console.error(`[TICK ERROR] ${error}`);
47
+ }
48
+ }
49
+ // Process first tick immediately
50
+ await processTick();
51
+ // Schedule regular ticks
52
+ const interval = setInterval(processTick, TICK_INTERVAL);
53
+ // Graceful shutdown
54
+ const shutdown = () => {
55
+ console.log('\n[TICK SERVER] Shutting down...');
56
+ clearInterval(interval);
57
+ console.log('[TICK SERVER] Stopped.');
58
+ process.exit(0);
59
+ };
60
+ process.on('SIGINT', shutdown);
61
+ process.on('SIGTERM', shutdown);
62
+ }
63
+ main().catch(console.error);
@@ -0,0 +1,78 @@
1
+ /**
2
+ * BURNRATE API Error Handling
3
+ * Standardized error responses
4
+ */
5
+ import { Context } from 'hono';
6
+ import { ZodError } from 'zod';
7
+ export declare const ErrorCodes: {
8
+ readonly MISSING_API_KEY: "MISSING_API_KEY";
9
+ readonly INVALID_API_KEY: "INVALID_API_KEY";
10
+ readonly INSUFFICIENT_TIER: "INSUFFICIENT_TIER";
11
+ readonly VALIDATION_ERROR: "VALIDATION_ERROR";
12
+ readonly INVALID_INPUT: "INVALID_INPUT";
13
+ readonly RATE_LIMITED: "RATE_LIMITED";
14
+ readonly DAILY_LIMIT_REACHED: "DAILY_LIMIT_REACHED";
15
+ readonly TICK_RATE_LIMITED: "TICK_RATE_LIMITED";
16
+ readonly NOT_FOUND: "NOT_FOUND";
17
+ readonly ZONE_NOT_FOUND: "ZONE_NOT_FOUND";
18
+ readonly PLAYER_NOT_FOUND: "PLAYER_NOT_FOUND";
19
+ readonly UNIT_NOT_FOUND: "UNIT_NOT_FOUND";
20
+ readonly SHIPMENT_NOT_FOUND: "SHIPMENT_NOT_FOUND";
21
+ readonly CONTRACT_NOT_FOUND: "CONTRACT_NOT_FOUND";
22
+ readonly FACTION_NOT_FOUND: "FACTION_NOT_FOUND";
23
+ readonly NOT_YOUR_RESOURCE: "NOT_YOUR_RESOURCE";
24
+ readonly INSUFFICIENT_PERMISSION: "INSUFFICIENT_PERMISSION";
25
+ readonly NOT_IN_FACTION: "NOT_IN_FACTION";
26
+ readonly ALREADY_IN_FACTION: "ALREADY_IN_FACTION";
27
+ readonly WRONG_ZONE_TYPE: "WRONG_ZONE_TYPE";
28
+ readonly WRONG_LOCATION: "WRONG_LOCATION";
29
+ readonly INSUFFICIENT_RESOURCES: "INSUFFICIENT_RESOURCES";
30
+ readonly INSUFFICIENT_CREDITS: "INSUFFICIENT_CREDITS";
31
+ readonly NO_ROUTE: "NO_ROUTE";
32
+ readonly UNIT_BUSY: "UNIT_BUSY";
33
+ readonly LICENSE_REQUIRED: "LICENSE_REQUIRED";
34
+ readonly NAME_TAKEN: "NAME_TAKEN";
35
+ readonly ALREADY_EXISTS: "ALREADY_EXISTS";
36
+ readonly CONFLICT: "CONFLICT";
37
+ readonly INVALID_STATE: "INVALID_STATE";
38
+ readonly INTERNAL_ERROR: "INTERNAL_ERROR";
39
+ readonly SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE";
40
+ };
41
+ export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
42
+ export declare class GameError extends Error {
43
+ code: ErrorCode;
44
+ statusCode: number;
45
+ details?: Record<string, unknown> | undefined;
46
+ constructor(code: ErrorCode, message: string, statusCode?: number, details?: Record<string, unknown> | undefined);
47
+ }
48
+ export declare class ValidationError extends GameError {
49
+ constructor(message: string, details?: Record<string, unknown>);
50
+ static fromZod(error: ZodError): ValidationError;
51
+ }
52
+ export declare class AuthError extends GameError {
53
+ constructor(code: ErrorCode, message: string);
54
+ }
55
+ export declare class NotFoundError extends GameError {
56
+ constructor(resource: string, id?: string);
57
+ }
58
+ export declare class RateLimitError extends GameError {
59
+ retryAfter?: number | undefined;
60
+ constructor(code: ErrorCode, message: string, retryAfter?: number | undefined);
61
+ }
62
+ export declare class PermissionError extends GameError {
63
+ constructor(message: string, code?: ErrorCode);
64
+ }
65
+ export declare class ConflictError extends GameError {
66
+ constructor(message: string, code?: ErrorCode);
67
+ }
68
+ export interface ErrorResponse {
69
+ error: {
70
+ code: ErrorCode;
71
+ message: string;
72
+ details?: Record<string, unknown>;
73
+ requestId: string;
74
+ };
75
+ }
76
+ export declare function errorResponse(c: Context, error: GameError | Error, requestId?: string): Response;
77
+ import { ZodSchema } from 'zod';
78
+ export declare function validateBody<T>(c: Context, schema: ZodSchema<T>): Promise<T>;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * BURNRATE API Error Handling
3
+ * Standardized error responses
4
+ */
5
+ import { ZodError } from 'zod';
6
+ import { v4 as uuid } from 'uuid';
7
+ // ============================================================================
8
+ // ERROR CODES
9
+ // ============================================================================
10
+ export const ErrorCodes = {
11
+ // Authentication
12
+ MISSING_API_KEY: 'MISSING_API_KEY',
13
+ INVALID_API_KEY: 'INVALID_API_KEY',
14
+ INSUFFICIENT_TIER: 'INSUFFICIENT_TIER',
15
+ // Validation
16
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
17
+ INVALID_INPUT: 'INVALID_INPUT',
18
+ // Rate Limiting
19
+ RATE_LIMITED: 'RATE_LIMITED',
20
+ DAILY_LIMIT_REACHED: 'DAILY_LIMIT_REACHED',
21
+ TICK_RATE_LIMITED: 'TICK_RATE_LIMITED',
22
+ // Resources
23
+ NOT_FOUND: 'NOT_FOUND',
24
+ ZONE_NOT_FOUND: 'ZONE_NOT_FOUND',
25
+ PLAYER_NOT_FOUND: 'PLAYER_NOT_FOUND',
26
+ UNIT_NOT_FOUND: 'UNIT_NOT_FOUND',
27
+ SHIPMENT_NOT_FOUND: 'SHIPMENT_NOT_FOUND',
28
+ CONTRACT_NOT_FOUND: 'CONTRACT_NOT_FOUND',
29
+ FACTION_NOT_FOUND: 'FACTION_NOT_FOUND',
30
+ // Permissions
31
+ NOT_YOUR_RESOURCE: 'NOT_YOUR_RESOURCE',
32
+ INSUFFICIENT_PERMISSION: 'INSUFFICIENT_PERMISSION',
33
+ NOT_IN_FACTION: 'NOT_IN_FACTION',
34
+ ALREADY_IN_FACTION: 'ALREADY_IN_FACTION',
35
+ // Game State
36
+ WRONG_ZONE_TYPE: 'WRONG_ZONE_TYPE',
37
+ WRONG_LOCATION: 'WRONG_LOCATION',
38
+ INSUFFICIENT_RESOURCES: 'INSUFFICIENT_RESOURCES',
39
+ INSUFFICIENT_CREDITS: 'INSUFFICIENT_CREDITS',
40
+ NO_ROUTE: 'NO_ROUTE',
41
+ UNIT_BUSY: 'UNIT_BUSY',
42
+ LICENSE_REQUIRED: 'LICENSE_REQUIRED',
43
+ // Conflicts
44
+ NAME_TAKEN: 'NAME_TAKEN',
45
+ ALREADY_EXISTS: 'ALREADY_EXISTS',
46
+ CONFLICT: 'CONFLICT',
47
+ // State
48
+ INVALID_STATE: 'INVALID_STATE',
49
+ // Server
50
+ INTERNAL_ERROR: 'INTERNAL_ERROR',
51
+ SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE'
52
+ };
53
+ // ============================================================================
54
+ // ERROR CLASSES
55
+ // ============================================================================
56
+ export class GameError extends Error {
57
+ code;
58
+ statusCode;
59
+ details;
60
+ constructor(code, message, statusCode = 400, details) {
61
+ super(message);
62
+ this.code = code;
63
+ this.statusCode = statusCode;
64
+ this.details = details;
65
+ this.name = 'GameError';
66
+ }
67
+ }
68
+ export class ValidationError extends GameError {
69
+ constructor(message, details) {
70
+ super(ErrorCodes.VALIDATION_ERROR, message, 400, details);
71
+ this.name = 'ValidationError';
72
+ }
73
+ static fromZod(error) {
74
+ const issues = error.issues.map(issue => ({
75
+ path: issue.path.join('.'),
76
+ message: issue.message
77
+ }));
78
+ return new ValidationError('Validation failed', { issues });
79
+ }
80
+ }
81
+ export class AuthError extends GameError {
82
+ constructor(code, message) {
83
+ super(code, message, 401);
84
+ this.name = 'AuthError';
85
+ }
86
+ }
87
+ export class NotFoundError extends GameError {
88
+ constructor(resource, id) {
89
+ super(ErrorCodes.NOT_FOUND, id ? `${resource} '${id}' not found` : `${resource} not found`, 404);
90
+ this.name = 'NotFoundError';
91
+ }
92
+ }
93
+ export class RateLimitError extends GameError {
94
+ retryAfter;
95
+ constructor(code, message, retryAfter) {
96
+ super(code, message, 429, retryAfter ? { retryAfter } : undefined);
97
+ this.retryAfter = retryAfter;
98
+ this.name = 'RateLimitError';
99
+ }
100
+ }
101
+ export class PermissionError extends GameError {
102
+ constructor(message, code = ErrorCodes.INSUFFICIENT_PERMISSION) {
103
+ super(code, message, 403);
104
+ this.name = 'PermissionError';
105
+ }
106
+ }
107
+ export class ConflictError extends GameError {
108
+ constructor(message, code = ErrorCodes.CONFLICT) {
109
+ super(code, message, 409);
110
+ this.name = 'ConflictError';
111
+ }
112
+ }
113
+ export function errorResponse(c, error, requestId) {
114
+ const rid = requestId || uuid().slice(0, 8);
115
+ if (error instanceof GameError) {
116
+ const response = {
117
+ error: {
118
+ code: error.code,
119
+ message: error.message,
120
+ requestId: rid
121
+ }
122
+ };
123
+ if (error.details) {
124
+ response.error.details = error.details;
125
+ }
126
+ const headers = {};
127
+ if (error instanceof RateLimitError && error.retryAfter) {
128
+ headers['Retry-After'] = String(error.retryAfter);
129
+ }
130
+ return c.json(response, error.statusCode, headers);
131
+ }
132
+ // Unknown error - log it but don't expose details
133
+ console.error(`[${rid}] Internal error:`, error);
134
+ return c.json({
135
+ error: {
136
+ code: ErrorCodes.INTERNAL_ERROR,
137
+ message: 'An internal error occurred',
138
+ requestId: rid
139
+ }
140
+ }, 500);
141
+ }
142
+ export async function validateBody(c, schema) {
143
+ try {
144
+ const body = await c.req.json();
145
+ return schema.parse(body);
146
+ }
147
+ catch (error) {
148
+ if (error instanceof ZodError) {
149
+ throw ValidationError.fromZod(error);
150
+ }
151
+ if (error instanceof SyntaxError) {
152
+ throw new ValidationError('Invalid JSON body');
153
+ }
154
+ throw error;
155
+ }
156
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * BURNRATE Rate Limiting
3
+ * In-memory rate limiter with sliding window
4
+ */
5
+ import { Context, Next } from 'hono';
6
+ interface RateLimitConfig {
7
+ windowMs: number;
8
+ maxRequests: number;
9
+ keyGenerator: (c: Context) => string;
10
+ }
11
+ export declare function rateLimitMiddleware(config?: Partial<RateLimitConfig>): (c: Context, next: Next) => Promise<void>;
12
+ /**
13
+ * Rate limiter that adjusts based on player tier
14
+ * Must be used AFTER auth middleware (requires player in context)
15
+ */
16
+ export declare function tierRateLimitMiddleware(): (c: Context, next: Next) => Promise<void>;
17
+ /**
18
+ * Stricter rate limit for write operations (mutations)
19
+ * 10 writes per minute for freelance, scaling up
20
+ */
21
+ export declare function writeRateLimitMiddleware(): (c: Context, next: Next) => Promise<void>;
22
+ export {};
@@ -0,0 +1,134 @@
1
+ /**
2
+ * BURNRATE Rate Limiting
3
+ * In-memory rate limiter with sliding window
4
+ */
5
+ import { RateLimitError, ErrorCodes } from './errors.js';
6
+ // In-memory store (use Redis in production for horizontal scaling)
7
+ const rateLimitStore = new Map();
8
+ // Clean up old entries periodically
9
+ setInterval(() => {
10
+ const now = Date.now();
11
+ const windowMs = 60 * 1000; // 1 minute window
12
+ for (const [key, entry] of rateLimitStore.entries()) {
13
+ if (now - entry.windowStart > windowMs * 2) {
14
+ rateLimitStore.delete(key);
15
+ }
16
+ }
17
+ }, 60 * 1000);
18
+ const DEFAULT_CONFIG = {
19
+ windowMs: 60 * 1000, // 1 minute
20
+ maxRequests: 100, // 100 requests per minute
21
+ keyGenerator: (c) => {
22
+ // Use API key if available, otherwise IP
23
+ const apiKey = c.req.header('X-API-Key');
24
+ if (apiKey)
25
+ return `api:${apiKey}`;
26
+ const forwarded = c.req.header('X-Forwarded-For');
27
+ const ip = forwarded ? forwarded.split(',')[0].trim() : 'unknown';
28
+ return `ip:${ip}`;
29
+ }
30
+ };
31
+ // Tier-based limits (requests per minute)
32
+ const TIER_RATE_LIMITS = {
33
+ freelance: 60,
34
+ operator: 120,
35
+ command: 300
36
+ };
37
+ // ============================================================================
38
+ // RATE LIMIT MIDDLEWARE
39
+ // ============================================================================
40
+ export function rateLimitMiddleware(config = {}) {
41
+ const { windowMs, maxRequests, keyGenerator } = { ...DEFAULT_CONFIG, ...config };
42
+ return async (c, next) => {
43
+ const key = keyGenerator(c);
44
+ const now = Date.now();
45
+ let entry = rateLimitStore.get(key);
46
+ // Start new window if needed
47
+ if (!entry || now - entry.windowStart > windowMs) {
48
+ entry = { count: 0, windowStart: now };
49
+ }
50
+ entry.count++;
51
+ rateLimitStore.set(key, entry);
52
+ // Check limit
53
+ if (entry.count > maxRequests) {
54
+ const retryAfter = Math.ceil((entry.windowStart + windowMs - now) / 1000);
55
+ throw new RateLimitError(ErrorCodes.RATE_LIMITED, `Rate limit exceeded. Try again in ${retryAfter} seconds.`, retryAfter);
56
+ }
57
+ // Add rate limit headers
58
+ c.header('X-RateLimit-Limit', String(maxRequests));
59
+ c.header('X-RateLimit-Remaining', String(Math.max(0, maxRequests - entry.count)));
60
+ c.header('X-RateLimit-Reset', String(Math.ceil((entry.windowStart + windowMs) / 1000)));
61
+ await next();
62
+ };
63
+ }
64
+ // ============================================================================
65
+ // TIER-AWARE RATE LIMIT
66
+ // ============================================================================
67
+ /**
68
+ * Rate limiter that adjusts based on player tier
69
+ * Must be used AFTER auth middleware (requires player in context)
70
+ */
71
+ export function tierRateLimitMiddleware() {
72
+ const windowMs = 60 * 1000; // 1 minute
73
+ return async (c, next) => {
74
+ const player = c.get('player');
75
+ // If no player (public endpoint), use base rate
76
+ if (!player) {
77
+ return rateLimitMiddleware({ maxRequests: 30 })(c, next);
78
+ }
79
+ const key = `player:${player.id}`;
80
+ const maxRequests = TIER_RATE_LIMITS[player.tier] || TIER_RATE_LIMITS.freelance;
81
+ const now = Date.now();
82
+ let entry = rateLimitStore.get(key);
83
+ if (!entry || now - entry.windowStart > windowMs) {
84
+ entry = { count: 0, windowStart: now };
85
+ }
86
+ entry.count++;
87
+ rateLimitStore.set(key, entry);
88
+ if (entry.count > maxRequests) {
89
+ const retryAfter = Math.ceil((entry.windowStart + windowMs - now) / 1000);
90
+ throw new RateLimitError(ErrorCodes.RATE_LIMITED, `Rate limit exceeded (${player.tier} tier: ${maxRequests}/min). Try again in ${retryAfter}s.`, retryAfter);
91
+ }
92
+ c.header('X-RateLimit-Limit', String(maxRequests));
93
+ c.header('X-RateLimit-Remaining', String(Math.max(0, maxRequests - entry.count)));
94
+ c.header('X-RateLimit-Reset', String(Math.ceil((entry.windowStart + windowMs) / 1000)));
95
+ await next();
96
+ };
97
+ }
98
+ // ============================================================================
99
+ // WRITE OPERATION LIMITER
100
+ // ============================================================================
101
+ const writeStore = new Map();
102
+ /**
103
+ * Stricter rate limit for write operations (mutations)
104
+ * 10 writes per minute for freelance, scaling up
105
+ */
106
+ export function writeRateLimitMiddleware() {
107
+ const windowMs = 60 * 1000;
108
+ const baseLimits = {
109
+ freelance: 20,
110
+ operator: 40,
111
+ command: 100
112
+ };
113
+ return async (c, next) => {
114
+ const player = c.get('player');
115
+ if (!player) {
116
+ await next();
117
+ return;
118
+ }
119
+ const key = `write:${player.id}`;
120
+ const maxRequests = baseLimits[player.tier] || baseLimits.freelance;
121
+ const now = Date.now();
122
+ let entry = writeStore.get(key);
123
+ if (!entry || now - entry.windowStart > windowMs) {
124
+ entry = { count: 0, windowStart: now };
125
+ }
126
+ entry.count++;
127
+ writeStore.set(key, entry);
128
+ if (entry.count > maxRequests) {
129
+ const retryAfter = Math.ceil((entry.windowStart + windowMs - now) / 1000);
130
+ throw new RateLimitError(ErrorCodes.RATE_LIMITED, `Write rate limit exceeded. Try again in ${retryAfter}s.`, retryAfter);
131
+ }
132
+ await next();
133
+ };
134
+ }
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BURNRATE Tick Server
4
+ * Automatically processes game ticks at regular intervals
5
+ *
6
+ * Default: 1 tick every 10 minutes (600,000ms)
7
+ * For testing: Use --interval flag to speed up
8
+ */
9
+ export {};
@@ -0,0 +1,114 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BURNRATE Tick Server
4
+ * Automatically processes game ticks at regular intervals
5
+ *
6
+ * Default: 1 tick every 10 minutes (600,000ms)
7
+ * For testing: Use --interval flag to speed up
8
+ */
9
+ import { GameDatabase } from '../db/database.js';
10
+ import { GameEngine } from '../core/engine.js';
11
+ import path from 'path';
12
+ import os from 'os';
13
+ import fs from 'fs';
14
+ // Configuration
15
+ const TICK_INTERVAL_MS = parseInt(process.env.BURNRATE_TICK_INTERVAL || '600000'); // 10 minutes default
16
+ const DB_DIR = path.join(os.homedir(), '.burnrate');
17
+ const DB_PATH = path.join(DB_DIR, 'game.db');
18
+ // Ensure directory exists
19
+ if (!fs.existsSync(DB_DIR)) {
20
+ fs.mkdirSync(DB_DIR, { recursive: true });
21
+ }
22
+ // Initialize
23
+ const db = new GameDatabase(DB_PATH);
24
+ const engine = new GameEngine(db);
25
+ // State
26
+ let running = true;
27
+ let tickCount = 0;
28
+ let lastTickTime = Date.now();
29
+ // Logging
30
+ function log(message) {
31
+ const timestamp = new Date().toISOString();
32
+ console.log(`[${timestamp}] ${message}`);
33
+ }
34
+ // Process a tick
35
+ function processTick() {
36
+ const startTime = Date.now();
37
+ const result = engine.processTick();
38
+ const duration = Date.now() - startTime;
39
+ tickCount++;
40
+ lastTickTime = Date.now();
41
+ // Log tick summary
42
+ const eventSummary = {};
43
+ for (const event of result.events) {
44
+ eventSummary[event.type] = (eventSummary[event.type] || 0) + 1;
45
+ }
46
+ log(`Tick ${result.tick} processed in ${duration}ms (${result.events.length} events)`);
47
+ // Log important events
48
+ for (const event of result.events) {
49
+ if (event.type === 'zone_state_changed') {
50
+ log(` Zone ${event.data.zoneName}: ${event.data.previousState} → ${event.data.newState}`);
51
+ }
52
+ if (event.type === 'zone_captured') {
53
+ log(` Zone ${event.data.zoneName} captured by ${event.data.newOwner || 'neutral'}`);
54
+ }
55
+ if (event.type === 'shipment_arrived') {
56
+ log(` Shipment ${event.data.shipmentId.slice(0, 8)} arrived at destination`);
57
+ }
58
+ if (event.type === 'shipment_intercepted') {
59
+ log(` Shipment ${event.data.shipmentId.slice(0, 8)} intercepted! ${event.data.outcome}`);
60
+ }
61
+ if (event.type === 'combat_resolved') {
62
+ log(` Combat: ${event.data.outcome} (E:${event.data.escortStrength} vs R:${event.data.raiderStrength})`);
63
+ }
64
+ }
65
+ }
66
+ // Main loop
67
+ function startTickLoop() {
68
+ log(`BURNRATE Tick Server started`);
69
+ log(`Tick interval: ${TICK_INTERVAL_MS}ms (${TICK_INTERVAL_MS / 60000} minutes)`);
70
+ log(`Database: ${DB_PATH}`);
71
+ log(`Current tick: ${db.getCurrentTick()}`);
72
+ log(`Press Ctrl+C to stop\n`);
73
+ // Process first tick immediately
74
+ processTick();
75
+ // Schedule subsequent ticks
76
+ const interval = setInterval(() => {
77
+ if (running) {
78
+ processTick();
79
+ }
80
+ }, TICK_INTERVAL_MS);
81
+ // Graceful shutdown
82
+ process.on('SIGINT', () => {
83
+ log('\nShutting down...');
84
+ running = false;
85
+ clearInterval(interval);
86
+ db.close();
87
+ log(`Processed ${tickCount} ticks total`);
88
+ process.exit(0);
89
+ });
90
+ process.on('SIGTERM', () => {
91
+ log('\nReceived SIGTERM, shutting down...');
92
+ running = false;
93
+ clearInterval(interval);
94
+ db.close();
95
+ process.exit(0);
96
+ });
97
+ }
98
+ // Status endpoint (simple file-based for CLI querying)
99
+ function writeStatus() {
100
+ const statusPath = path.join(DB_DIR, 'server-status.json');
101
+ const status = {
102
+ running,
103
+ tickCount,
104
+ currentTick: db.getCurrentTick(),
105
+ lastTickTime: new Date(lastTickTime).toISOString(),
106
+ intervalMs: TICK_INTERVAL_MS,
107
+ pid: process.pid
108
+ };
109
+ fs.writeFileSync(statusPath, JSON.stringify(status, null, 2));
110
+ }
111
+ // Update status every tick
112
+ setInterval(writeStatus, 5000);
113
+ // Start the server
114
+ startTickLoop();