Files
AIHostingTycoon/packages/game-engine/src/engine.ts
T
josh fdc8e544ae Initial scaffold: AI Tycoon monorepo with core game loop
Turborepo monorepo with three packages:
- packages/shared: TypeScript types for all 14 game systems + balance constants + formatting utils
- packages/game-engine: Pure TS simulation engine with tick processor, economy, infrastructure, compute, research, market, and reputation systems
- apps/web: React + Vite + Tailwind + Zustand frontend with sidebar dashboard layout, new game screen, dashboard with charts, infrastructure management, and model training pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-24 16:53:46 -04:00

83 lines
2.4 KiB
TypeScript

import type { GameState } from '@ai-tycoon/shared';
import { processTick } from './tick';
export interface GameEngineCallbacks {
getState: () => GameState;
setState: (partial: Partial<GameState>) => void;
onTick?: (tickCount: number) => void;
onEraChange?: (era: GameState['meta']['currentEra']) => void;
}
export class GameEngine {
private callbacks: GameEngineCallbacks;
private lastFrameTime = 0;
private accumulator = 0;
private animFrameId: number | null = null;
private tickIntervalMs = 1000;
constructor(callbacks: GameEngineCallbacks) {
this.callbacks = callbacks;
}
start(): void {
if (this.animFrameId !== null) return;
this.lastFrameTime = performance.now();
this.accumulator = 0;
this.loop(this.lastFrameTime);
}
stop(): void {
if (this.animFrameId !== null) {
cancelAnimationFrame(this.animFrameId);
this.animFrameId = null;
}
}
setSpeed(speed: number): void {
this.tickIntervalMs = 1000 / speed;
}
processOfflineTicks(missedTicks: number): { revenue: number; expenses: number; ticksProcessed: number } {
let totalRevenue = 0;
let totalExpenses = 0;
for (let i = 0; i < missedTicks; i++) {
const state = this.callbacks.getState();
const result = processTick(state);
this.callbacks.setState(result);
totalRevenue += result.economy?.revenuePerTick ?? 0;
totalExpenses += result.economy?.expensesPerTick ?? 0;
}
return { revenue: totalRevenue, expenses: totalExpenses, ticksProcessed: missedTicks };
}
private loop = (now: number): void => {
const delta = now - this.lastFrameTime;
this.lastFrameTime = now;
const state = this.callbacks.getState();
if (!state.meta.isPaused) {
this.accumulator += delta;
let ticksThisFrame = 0;
const maxTicksPerFrame = 10;
while (this.accumulator >= this.tickIntervalMs && ticksThisFrame < maxTicksPerFrame) {
const currentState = this.callbacks.getState();
const result = processTick(currentState);
this.callbacks.setState(result);
this.accumulator -= this.tickIntervalMs;
ticksThisFrame++;
this.callbacks.onTick?.(currentState.meta.tickCount + 1);
}
if (this.accumulator > this.tickIntervalMs * maxTicksPerFrame) {
this.accumulator = 0;
}
}
this.animFrameId = requestAnimationFrame(this.loop);
};
}