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>
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user