The random events (GPU shortages, regulatory hearings, PR crises, etc.) added interruption without enough gameplay value. Removed all event types, definitions (~1800 lines of event data), the event processor, EventModal UI, store actions, and tick integration. Updated docs to reflect the removal. Bundle size drops ~47kB. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,84 +0,0 @@
|
|||||||
import { AlertTriangle, Newspaper, Building2, Users, TrendingUp, X } from 'lucide-react';
|
|
||||||
import { useGameStore } from '@/store';
|
|
||||||
import type { ActiveEvent, EventCategory } from '@ai-tycoon/shared';
|
|
||||||
|
|
||||||
const CATEGORY_ICONS: Record<EventCategory, typeof AlertTriangle> = {
|
|
||||||
industry: Newspaper,
|
|
||||||
regulatory: Building2,
|
|
||||||
pr: Users,
|
|
||||||
internal: AlertTriangle,
|
|
||||||
market: TrendingUp,
|
|
||||||
};
|
|
||||||
|
|
||||||
const CATEGORY_COLORS: Record<EventCategory, string> = {
|
|
||||||
industry: 'border-blue-500/50 bg-blue-500/5',
|
|
||||||
regulatory: 'border-yellow-500/50 bg-yellow-500/5',
|
|
||||||
pr: 'border-purple-500/50 bg-purple-500/5',
|
|
||||||
internal: 'border-orange-500/50 bg-orange-500/5',
|
|
||||||
market: 'border-green-500/50 bg-green-500/5',
|
|
||||||
};
|
|
||||||
|
|
||||||
export function EventModal() {
|
|
||||||
const activeEvents = useGameStore((s) => s.events.activeEvents);
|
|
||||||
const resolveEvent = useGameStore((s) => s.resolveEvent);
|
|
||||||
|
|
||||||
if (activeEvents.length === 0) return null;
|
|
||||||
|
|
||||||
const event = activeEvents[0];
|
|
||||||
const Icon = CATEGORY_ICONS[event.category];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4">
|
|
||||||
<div className={`bg-surface-900 border-2 rounded-xl max-w-lg w-full shadow-2xl ${CATEGORY_COLORS[event.category]}`}>
|
|
||||||
<div className="p-5">
|
|
||||||
<div className="flex items-start justify-between mb-3">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Icon size={20} className="text-accent-light" />
|
|
||||||
<h3 className="text-lg font-bold">{event.title}</h3>
|
|
||||||
</div>
|
|
||||||
<span className="text-xs text-surface-400 uppercase">{event.category}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-sm text-surface-300 mb-5 leading-relaxed">{event.description}</p>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
{event.choices.map((choice, idx) => (
|
|
||||||
<button
|
|
||||||
key={idx}
|
|
||||||
onClick={() => resolveEvent(event.instanceId, idx)}
|
|
||||||
className="w-full text-left bg-surface-800 hover:bg-surface-700 border border-surface-600 hover:border-surface-500 rounded-lg p-3 transition-all group"
|
|
||||||
>
|
|
||||||
<div className="text-sm font-medium group-hover:text-accent-light transition-colors">
|
|
||||||
{choice.label}
|
|
||||||
</div>
|
|
||||||
<div className="text-xs text-surface-400 mt-1">{choice.description}</div>
|
|
||||||
<div className="flex gap-2 mt-2">
|
|
||||||
{choice.consequences.map((c, i) => (
|
|
||||||
<ConsequenceTag key={i} type={c.type} value={c.value} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConsequenceTag({ type, value }: { type: string; value: number }) {
|
|
||||||
const isPositive = value > 0;
|
|
||||||
const label = type === 'money' ? `$${Math.abs(value).toLocaleString()}`
|
|
||||||
: type === 'reputation' ? `${Math.abs(value)} rep`
|
|
||||||
: type === 'talent' ? `${Math.abs(value)} talent`
|
|
||||||
: type === 'research_speed' ? `${Math.round(Math.abs(value) * 100)}% R&D`
|
|
||||||
: `${type}: ${value}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<span className={`text-[10px] px-1.5 py-0.5 rounded ${
|
|
||||||
isPositive ? 'bg-success/20 text-success' : 'bg-danger/20 text-danger'
|
|
||||||
}`}>
|
|
||||||
{isPositive ? '+' : '-'}{label}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,7 @@ export function OfflineCatchUp({ missedTicks, onComplete }: { missedTicks: numbe
|
|||||||
meta: s.meta, economy: s.economy, infrastructure: s.infrastructure,
|
meta: s.meta, economy: s.economy, infrastructure: s.infrastructure,
|
||||||
compute: s.compute, research: s.research, models: s.models,
|
compute: s.compute, research: s.research, models: s.models,
|
||||||
market: s.market, competitors: s.competitors, talent: s.talent,
|
market: s.market, competitors: s.competitors, talent: s.talent,
|
||||||
data: s.data, reputation: s.reputation, events: s.events,
|
data: s.data, reputation: s.reputation,
|
||||||
achievements: s.achievements,
|
achievements: s.achievements,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Sidebar } from './Sidebar';
|
import { Sidebar } from './Sidebar';
|
||||||
import { TopBar } from './TopBar';
|
import { TopBar } from './TopBar';
|
||||||
import { ToastContainer } from '@/components/common/ToastContainer';
|
import { ToastContainer } from '@/components/common/ToastContainer';
|
||||||
import { EventModal } from '@/components/game/EventModal';
|
|
||||||
import { useGameStore } from '@/store';
|
import { useGameStore } from '@/store';
|
||||||
import { DashboardPage } from '@/pages/DashboardPage';
|
import { DashboardPage } from '@/pages/DashboardPage';
|
||||||
import { InfrastructurePage } from '@/pages/InfrastructurePage';
|
import { InfrastructurePage } from '@/pages/InfrastructurePage';
|
||||||
@@ -29,7 +28,6 @@ export function MainLayout() {
|
|||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
<EventModal />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { GameEngine, setEventDefinitions, setAchievementDefinitions, EVENT_DEFINITIONS, ACHIEVEMENT_DEFINITIONS } from '@ai-tycoon/game-engine';
|
import { GameEngine, setAchievementDefinitions, ACHIEVEMENT_DEFINITIONS } from '@ai-tycoon/game-engine';
|
||||||
import type { TickNotification } from '@ai-tycoon/game-engine';
|
import type { TickNotification } from '@ai-tycoon/game-engine';
|
||||||
import { useGameStore } from '@/store';
|
import { useGameStore } from '@/store';
|
||||||
|
|
||||||
@@ -11,7 +11,6 @@ export function useGameLoop(skip = false) {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!companyName || skip) return;
|
if (!companyName || skip) return;
|
||||||
|
|
||||||
setEventDefinitions(EVENT_DEFINITIONS);
|
|
||||||
setAchievementDefinitions(ACHIEVEMENT_DEFINITIONS);
|
setAchievementDefinitions(ACHIEVEMENT_DEFINITIONS);
|
||||||
|
|
||||||
const engine = new GameEngine({
|
const engine = new GameEngine({
|
||||||
@@ -29,7 +28,6 @@ export function useGameLoop(skip = false) {
|
|||||||
talent: state.talent,
|
talent: state.talent,
|
||||||
data: state.data,
|
data: state.data,
|
||||||
reputation: state.reputation,
|
reputation: state.reputation,
|
||||||
events: state.events,
|
|
||||||
achievements: state.achievements,
|
achievements: state.achievements,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function FinancePage() {
|
|||||||
meta: state.meta, economy: state.economy, infrastructure: state.infrastructure,
|
meta: state.meta, economy: state.economy, infrastructure: state.infrastructure,
|
||||||
compute: state.compute, research: state.research, models: state.models,
|
compute: state.compute, research: state.research, models: state.models,
|
||||||
market: state.market, competitors: state.competitors, talent: state.talent,
|
market: state.market, competitors: state.competitors, talent: state.talent,
|
||||||
data: state.data, reputation: state.reputation, events: state.events,
|
data: state.data, reputation: state.reputation,
|
||||||
achievements: state.achievements,
|
achievements: state.achievements,
|
||||||
};
|
};
|
||||||
const fundingStatus = canRaiseFunding(gameStateForFunding);
|
const fundingStatus = canRaiseFunding(gameStateForFunding);
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import type {
|
|||||||
EconomyState, InfrastructureState, ComputeState,
|
EconomyState, InfrastructureState, ComputeState,
|
||||||
ResearchState, ModelsState, MarketState,
|
ResearchState, ModelsState, MarketState,
|
||||||
CompetitorState, TalentState, DataState,
|
CompetitorState, TalentState, DataState,
|
||||||
ReputationState, EventState, AchievementState,
|
ReputationState, AchievementState,
|
||||||
DataCenter, DCTier, RackSkuId, TrainingJob,
|
DataCenter, DCTier, RackSkuId, TrainingJob,
|
||||||
ActiveResearch, EventConsequence, OwnedDataset, LocationId,
|
ActiveResearch, OwnedDataset, LocationId,
|
||||||
} from '@ai-tycoon/shared';
|
} from '@ai-tycoon/shared';
|
||||||
import type { FundingRoundType, OverloadPolicy, TuningPreset, ModelTuning } from '@ai-tycoon/shared';
|
import type { FundingRoundType, OverloadPolicy, TuningPreset, ModelTuning } from '@ai-tycoon/shared';
|
||||||
import {
|
import {
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
INITIAL_ECONOMY, INITIAL_INFRASTRUCTURE, INITIAL_COMPUTE,
|
INITIAL_ECONOMY, INITIAL_INFRASTRUCTURE, INITIAL_COMPUTE,
|
||||||
INITIAL_RESEARCH, INITIAL_MODELS, INITIAL_MARKET,
|
INITIAL_RESEARCH, INITIAL_MODELS, INITIAL_MARKET,
|
||||||
INITIAL_COMPETITORS, INITIAL_TALENT, INITIAL_DATA,
|
INITIAL_COMPETITORS, INITIAL_TALENT, INITIAL_DATA,
|
||||||
INITIAL_REPUTATION, INITIAL_EVENTS, INITIAL_ACHIEVEMENTS,
|
INITIAL_REPUTATION, INITIAL_ACHIEVEMENTS,
|
||||||
DC_TIER_CONFIGS, RACK_SKU_CONFIGS,
|
DC_TIER_CONFIGS, RACK_SKU_CONFIGS,
|
||||||
PIPELINE_ORDER_BASE_TICKS, DC_UPGRADE_COST_FRACTION, DC_UPGRADE_INCREMENT,
|
PIPELINE_ORDER_BASE_TICKS, DC_UPGRADE_COST_FRACTION, DC_UPGRADE_INCREMENT,
|
||||||
FUNDING_ROUNDS,
|
FUNDING_ROUNDS,
|
||||||
@@ -57,7 +57,6 @@ interface Actions {
|
|||||||
setProductPricing: (productLineId: string, field: string, value: number) => void;
|
setProductPricing: (productLineId: string, field: string, value: number) => void;
|
||||||
toggleProductLine: (productLineId: string) => void;
|
toggleProductLine: (productLineId: string) => void;
|
||||||
startResearch: (research: ActiveResearch) => void;
|
startResearch: (research: ActiveResearch) => void;
|
||||||
resolveEvent: (instanceId: string, choiceIndex: number) => void;
|
|
||||||
hireDepartment: (departmentId: string, count: number) => void;
|
hireDepartment: (departmentId: string, count: number) => void;
|
||||||
purchaseDataset: (dataset: OwnedDataset, cost: number) => void;
|
purchaseDataset: (dataset: OwnedDataset, cost: number) => void;
|
||||||
raiseFunding: (roundType: FundingRoundType) => void;
|
raiseFunding: (roundType: FundingRoundType) => void;
|
||||||
@@ -93,7 +92,6 @@ const initialGameState: GameState = {
|
|||||||
talent: INITIAL_TALENT,
|
talent: INITIAL_TALENT,
|
||||||
data: INITIAL_DATA,
|
data: INITIAL_DATA,
|
||||||
reputation: INITIAL_REPUTATION,
|
reputation: INITIAL_REPUTATION,
|
||||||
events: INITIAL_EVENTS,
|
|
||||||
achievements: INITIAL_ACHIEVEMENTS,
|
achievements: INITIAL_ACHIEVEMENTS,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -293,55 +291,6 @@ export const useGameStore = create<Store>()(
|
|||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|
||||||
resolveEvent: (instanceId, choiceIndex) => set((s) => {
|
|
||||||
const event = s.events.activeEvents.find(e => e.instanceId === instanceId);
|
|
||||||
if (!event) return s;
|
|
||||||
|
|
||||||
const choice = event.choices[choiceIndex];
|
|
||||||
if (!choice) return s;
|
|
||||||
|
|
||||||
let money = s.economy.money;
|
|
||||||
let reputation = { ...s.reputation };
|
|
||||||
let talent = { ...s.talent };
|
|
||||||
const consequences = choice.consequences;
|
|
||||||
|
|
||||||
for (const c of consequences) {
|
|
||||||
switch (c.type) {
|
|
||||||
case 'money': money += c.value; break;
|
|
||||||
case 'reputation': reputation = { ...reputation, score: Math.min(100, Math.max(0, reputation.score + c.value)), publicPerception: Math.min(100, Math.max(0, reputation.publicPerception + c.value)) }; break;
|
|
||||||
case 'regulation': reputation = { ...reputation, regulatoryStanding: Math.min(100, Math.max(0, reputation.regulatoryStanding + c.value)) }; break;
|
|
||||||
case 'talent': {
|
|
||||||
const dept = c.target as keyof typeof talent.departments | undefined;
|
|
||||||
if (dept && talent.departments[dept]) {
|
|
||||||
talent = { ...talent, departments: { ...talent.departments, [dept]: { ...talent.departments[dept], headcount: Math.max(0, talent.departments[dept].headcount + c.value) } } };
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
economy: { ...s.economy, money: Math.max(0, money) },
|
|
||||||
reputation,
|
|
||||||
talent,
|
|
||||||
events: {
|
|
||||||
...s.events,
|
|
||||||
activeEvents: s.events.activeEvents.filter(e => e.instanceId !== instanceId),
|
|
||||||
eventHistory: [
|
|
||||||
...s.events.eventHistory,
|
|
||||||
{
|
|
||||||
eventId: event.eventId,
|
|
||||||
instanceId,
|
|
||||||
title: event.title,
|
|
||||||
category: event.category,
|
|
||||||
tick: s.meta.tickCount,
|
|
||||||
chosenOptionIndex: choiceIndex,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
|
|
||||||
hireDepartment: (departmentId, count) => set((s) => {
|
hireDepartment: (departmentId, count) => set((s) => {
|
||||||
const costPerHire = 2000;
|
const costPerHire = 2000;
|
||||||
const totalCost = costPerHire * count;
|
const totalCost = costPerHire * count;
|
||||||
|
|||||||
+8
-18
@@ -24,7 +24,7 @@ ai-tycoon/
|
|||||||
├── apps/
|
├── apps/
|
||||||
│ ├── web/ # React frontend (Vite)
|
│ ├── web/ # React frontend (Vite)
|
||||||
│ │ └── src/
|
│ │ └── src/
|
||||||
│ │ ├── components/ # layout/, common/, charts/, events/, game/
|
│ │ ├── components/ # layout/, common/, charts/, game/
|
||||||
│ │ ├── pages/ # One page per game system
|
│ │ ├── pages/ # One page per game system
|
||||||
│ │ ├── store/ # Zustand store with slice pattern
|
│ │ ├── store/ # Zustand store with slice pattern
|
||||||
│ │ ├── hooks/ # useGameLoop, useOfflineCatchUp, etc.
|
│ │ ├── hooks/ # useGameLoop, useOfflineCatchUp, etc.
|
||||||
@@ -49,7 +49,7 @@ ai-tycoon/
|
|||||||
├── engine.ts # GameEngine class (tick scheduling)
|
├── engine.ts # GameEngine class (tick scheduling)
|
||||||
├── tick.ts # Tick processor (orchestrates systems)
|
├── tick.ts # Tick processor (orchestrates systems)
|
||||||
├── systems/ # One file per simulation system
|
├── systems/ # One file per simulation system
|
||||||
└── data/ # Event definitions, tech tree, datasets
|
└── data/ # Tech tree, achievements, datasets
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tick System
|
## Tick System
|
||||||
@@ -83,10 +83,9 @@ Each tick runs these systems sequentially, since later systems depend on earlier
|
|||||||
8. Economy — Revenue, expenses, net cash flow
|
8. Economy — Revenue, expenses, net cash flow
|
||||||
9. Data — Data acquisition, user data flywheel
|
9. Data — Data acquisition, user data flywheel
|
||||||
10. Competitors — AI rival decisions and actions
|
10. Competitors — AI rival decisions and actions
|
||||||
11. Events — Random + condition-triggered events
|
11. Era check — Threshold-based era transitions
|
||||||
12. Era check — Threshold-based era transitions
|
12. Valuation — Dynamic company valuation
|
||||||
13. Valuation — Dynamic company valuation
|
13. Achievements — Milestone checks (every 10 ticks)
|
||||||
14. Achievements — Milestone checks (every 10 ticks)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Offline Catch-Up
|
### Offline Catch-Up
|
||||||
@@ -118,7 +117,6 @@ The store uses a slice pattern with 14 slices, each owning a portion of the game
|
|||||||
| `talentSlice` | Departments, headcount, morale, hiring |
|
| `talentSlice` | Departments, headcount, morale, hiring |
|
||||||
| `dataSlice` | Datasets, quality, user data generation |
|
| `dataSlice` | Datasets, quality, user data generation |
|
||||||
| `reputationSlice` | Safety record, public perception, regulatory standing |
|
| `reputationSlice` | Safety record, public perception, regulatory standing |
|
||||||
| `eventSlice` | Active events, history, cooldowns |
|
|
||||||
| `achievementSlice` | Unlocked achievements |
|
| `achievementSlice` | Unlocked achievements |
|
||||||
| `uiSlice` | Active page, notifications, modals (not persisted) |
|
| `uiSlice` | Active page, notifications, modals (not persisted) |
|
||||||
|
|
||||||
@@ -188,20 +186,12 @@ Training a model:
|
|||||||
Composite score (0-100) from four factors:
|
Composite score (0-100) from four factors:
|
||||||
|
|
||||||
- **Safety record** (30%): Damaged by safety incidents
|
- **Safety record** (30%): Damaged by safety incidents
|
||||||
- **Public perception** (30%): Affected by events and incidents
|
- **Public perception** (30%): Affected by incidents and open-sourcing models
|
||||||
- **Employee satisfaction** (20%): Driven by department morale averages
|
- **Employee satisfaction** (20%): Driven by department morale averages
|
||||||
- **Regulatory standing** (20%): `50 + safetyResearch * 8 - eraIndex * 5`
|
- **Regulatory standing** (20%): `50 + safetyResearch * 8 - eraIndex * 5`
|
||||||
|
|
||||||
Safety incidents trigger when deployed models have safety scores below `LOW_SAFETY_THRESHOLD` (40), checked every 60 ticks with probability `SAFETY_INCIDENT_PROBABILITY_BASE * (threshold - safetyLevel)`.
|
Safety incidents trigger when deployed models have safety scores below `LOW_SAFETY_THRESHOLD` (40), checked every 60 ticks with probability `SAFETY_INCIDENT_PROBABILITY_BASE * (threshold - safetyLevel)`.
|
||||||
|
|
||||||
### Event System (`eventSystem.ts`)
|
|
||||||
|
|
||||||
- 40+ event definitions across 6 categories
|
|
||||||
- Events have conditions (era, tick count, state thresholds), weights, cooldowns, and max occurrences
|
|
||||||
- Most events present 2-3 choices with consequences affecting money, reputation, compute, or other state
|
|
||||||
- Template system with variable interpolation prevents repetition
|
|
||||||
- Geopolitical events (export controls, energy crises, natural disasters) affect specific regions
|
|
||||||
|
|
||||||
### Competitor System (`competitorSystem.ts`)
|
### Competitor System (`competitorSystem.ts`)
|
||||||
|
|
||||||
- 3+ AI rival labs with archetypes (safety-first, move-fast, big tech, open-source)
|
- 3+ AI rival labs with archetypes (safety-first, move-fast, big tech, open-source)
|
||||||
@@ -283,7 +273,7 @@ Anonymous-first: players get a UUID token on first visit. Optional email/passwor
|
|||||||
## Performance Considerations
|
## Performance Considerations
|
||||||
|
|
||||||
- Game engine runs outside React's render cycle; single `setState` per tick
|
- Game engine runs outside React's render cycle; single `setState` per tick
|
||||||
- Achievements check every 10 ticks, events every 30 ticks
|
- Achievements check every 10 ticks
|
||||||
- History arrays use circular buffers with configurable max sizes
|
- History arrays use circular buffers with configurable max sizes
|
||||||
- Snapshots sample at intervals (every 60-120 ticks), not every tick
|
- Snapshots sample at intervals (every 60-120 ticks), not every tick
|
||||||
- All systems are bounded O(n) where n is small (number of GPUs, models, competitors)
|
- All systems are bounded O(n) where n is small (number of GPUs, models, competitors)
|
||||||
@@ -299,4 +289,4 @@ Anonymous-first: players get a UUID token on first visit. Optional email/passwor
|
|||||||
|
|
||||||
4. **Notifications via `_notifications`**: The tick processor attaches notifications as a side-channel property on the result, keeping the return type clean while enabling the UI to show contextual alerts.
|
4. **Notifications via `_notifications`**: The tick processor attaches notifications as a side-channel property on the result, keeping the return type clean while enabling the UI to show contextual alerts.
|
||||||
|
|
||||||
5. **Cached definitions**: Event and achievement definitions are set once at game start via `setEventDefinitions` / `setAchievementDefinitions`, avoiding repeated imports in the hot tick path.
|
5. **Cached definitions**: Achievement definitions are set once at game start via `setAchievementDefinitions`, avoiding repeated imports in the hot tick path.
|
||||||
|
|||||||
@@ -145,19 +145,6 @@ Three rival AI labs compete with you. Each has a personality:
|
|||||||
|
|
||||||
In later eras (Big Tech and AGI), you can **acquire** competitors, absorbing their talent and technology.
|
In later eras (Big Tech and AGI), you can **acquire** competitors, absorbing their talent and technology.
|
||||||
|
|
||||||
### Events
|
|
||||||
|
|
||||||
Random and conditional events keep the game dynamic. Categories include:
|
|
||||||
|
|
||||||
- **Industry**: Breakthroughs, open-source releases, benchmarks
|
|
||||||
- **Regulatory**: Hearings, compliance requirements, AI bills
|
|
||||||
- **PR/Cultural**: Media coverage, safety debates, public opinion shifts
|
|
||||||
- **Internal**: Employee issues, technical problems
|
|
||||||
- **Market**: Demand spikes, pricing pressure
|
|
||||||
- **Geopolitical**: Export controls, energy crises, natural disasters
|
|
||||||
|
|
||||||
Most events present 2-3 choices with meaningful tradeoffs. Some trigger chain events with delayed consequences.
|
|
||||||
|
|
||||||
### Funding
|
### Funding
|
||||||
|
|
||||||
Raise capital through VC rounds as you grow:
|
Raise capital through VC rounds as you grow:
|
||||||
@@ -215,7 +202,6 @@ Invest in multiple specializations and diverse products. Spread your compute acr
|
|||||||
- **Timing funding rounds matters.** Raise too early and you give up equity cheaply. Raise too late and you run out of runway.
|
- **Timing funding rounds matters.** Raise too early and you give up equity cheaply. Raise too late and you run out of runway.
|
||||||
- **Safety research compounds.** Each safety project improves all future models.
|
- **Safety research compounds.** Each safety project improves all future models.
|
||||||
- **Check competitor activity.** If a rival just released a strong model, expect to lose some subscribers unless you respond.
|
- **Check competitor activity.** If a rival just released a strong model, expect to lose some subscribers unless you respond.
|
||||||
- **Events have lasting consequences.** Read the options carefully — some choices trigger follow-up events.
|
|
||||||
- **The data flywheel is real.** More users generate more data, which trains better models, which attract more users.
|
- **The data flywheel is real.** More users generate more data, which trains better models, which attract more users.
|
||||||
- **Deploy your models.** A trained model sitting idle generates zero revenue.
|
- **Deploy your models.** A trained model sitting idle generates zero revenue.
|
||||||
- **Use speed controls.** Pause when making big decisions. Speed up during waiting periods.
|
- **Use speed controls.** Pause when making big decisions. Speed up during waiting periods.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,9 @@
|
|||||||
export { GameEngine } from './engine';
|
export { GameEngine } from './engine';
|
||||||
export { processTick, setEventDefinitions, setAchievementDefinitions } from './tick';
|
export { processTick, setAchievementDefinitions } from './tick';
|
||||||
export type { TickNotification } from './tick';
|
export type { TickNotification } from './tick';
|
||||||
export { getAvailableResearch, getResearchNode } from './systems/researchSystem';
|
export { getAvailableResearch, getResearchNode } from './systems/researchSystem';
|
||||||
export { canRaiseFunding, getNextFundingRound, computeValuation } from './systems/fundingSystem';
|
export { canRaiseFunding, getNextFundingRound, computeValuation } from './systems/fundingSystem';
|
||||||
export { TECH_TREE } from './data/techTree';
|
export { TECH_TREE } from './data/techTree';
|
||||||
export { INITIAL_RIVALS } from './data/competitors';
|
export { INITIAL_RIVALS } from './data/competitors';
|
||||||
export { KEY_HIRE_POOL } from './data/keyHires';
|
export { KEY_HIRE_POOL } from './data/keyHires';
|
||||||
export { EVENT_DEFINITIONS } from './data/events';
|
|
||||||
export { ACHIEVEMENT_DEFINITIONS } from './data/achievements';
|
export { ACHIEVEMENT_DEFINITIONS } from './data/achievements';
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import type { GameState, EventState, ActiveEvent, EventDefinition, EventCondition } from '@ai-tycoon/shared';
|
|
||||||
import { uuid } from '@ai-tycoon/shared';
|
|
||||||
|
|
||||||
export interface EventTickResult {
|
|
||||||
events: EventState;
|
|
||||||
newEvents: ActiveEvent[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function processEvents(
|
|
||||||
state: GameState,
|
|
||||||
definitions: EventDefinition[],
|
|
||||||
): EventTickResult {
|
|
||||||
const tick = state.meta.tickCount;
|
|
||||||
const events = { ...state.events };
|
|
||||||
const newEvents: ActiveEvent[] = [];
|
|
||||||
|
|
||||||
// Remove expired events (auto-choose default)
|
|
||||||
const stillActive: ActiveEvent[] = [];
|
|
||||||
for (const event of events.activeEvents) {
|
|
||||||
if (tick >= event.expiresAtTick) {
|
|
||||||
events.eventHistory = [
|
|
||||||
...events.eventHistory,
|
|
||||||
{
|
|
||||||
eventId: event.eventId,
|
|
||||||
instanceId: event.instanceId,
|
|
||||||
title: event.title,
|
|
||||||
category: event.category,
|
|
||||||
tick,
|
|
||||||
chosenOptionIndex: event.defaultChoiceIndex,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
stillActive.push(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
events.activeEvents = stillActive;
|
|
||||||
|
|
||||||
if (events.eventHistory.length > 50) {
|
|
||||||
events.eventHistory = events.eventHistory.slice(-50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only try to fire a new event every 30 ticks, and max 1 active at a time
|
|
||||||
if (tick % 30 !== 0 || events.activeEvents.length > 0) {
|
|
||||||
return { events, newEvents };
|
|
||||||
}
|
|
||||||
|
|
||||||
const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi'];
|
|
||||||
const currentEraIdx = eraOrder.indexOf(state.meta.currentEra);
|
|
||||||
|
|
||||||
const eligible = definitions.filter(def => {
|
|
||||||
if (!def.eras.some(e => eraOrder.indexOf(e) <= currentEraIdx)) return false;
|
|
||||||
const occ = events.eventOccurrences[def.id] ?? 0;
|
|
||||||
if (occ >= def.maxOccurrences) return false;
|
|
||||||
const cooldownEnd = events.eventCooldowns[def.id] ?? 0;
|
|
||||||
if (tick < cooldownEnd) return false;
|
|
||||||
if (def.prerequisites.some(p => !state.research.completedResearch.includes(p))) return false;
|
|
||||||
if (!def.conditions.every(c => evaluateCondition(state, c))) return false;
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (eligible.length === 0) return { events, newEvents };
|
|
||||||
|
|
||||||
const totalWeight = eligible.reduce((s, d) => s + d.weight, 0);
|
|
||||||
let roll = Math.random() * totalWeight;
|
|
||||||
let chosen: EventDefinition | null = null;
|
|
||||||
for (const def of eligible) {
|
|
||||||
roll -= def.weight;
|
|
||||||
if (roll <= 0) { chosen = def; break; }
|
|
||||||
}
|
|
||||||
if (!chosen) return { events, newEvents };
|
|
||||||
|
|
||||||
// Only fire with 30% probability per check to space events out
|
|
||||||
if (Math.random() > 0.3) return { events, newEvents };
|
|
||||||
|
|
||||||
const activeEvent: ActiveEvent = {
|
|
||||||
eventId: chosen.id,
|
|
||||||
instanceId: uuid(),
|
|
||||||
triggeredAtTick: tick,
|
|
||||||
expiresAtTick: tick + chosen.expiryTicks,
|
|
||||||
title: chosen.title,
|
|
||||||
description: chosen.descriptionTemplate,
|
|
||||||
category: chosen.category,
|
|
||||||
choices: chosen.choices,
|
|
||||||
defaultChoiceIndex: chosen.defaultChoiceIndex,
|
|
||||||
};
|
|
||||||
|
|
||||||
events.activeEvents = [...events.activeEvents, activeEvent];
|
|
||||||
events.eventCooldowns = { ...events.eventCooldowns, [chosen.id]: tick + chosen.cooldownTicks };
|
|
||||||
events.eventOccurrences = {
|
|
||||||
...events.eventOccurrences,
|
|
||||||
[chosen.id]: (events.eventOccurrences[chosen.id] ?? 0) + 1,
|
|
||||||
};
|
|
||||||
newEvents.push(activeEvent);
|
|
||||||
|
|
||||||
return { events, newEvents };
|
|
||||||
}
|
|
||||||
|
|
||||||
function evaluateCondition(state: GameState, condition: EventCondition): boolean {
|
|
||||||
const value = getNestedValue(state, condition.field);
|
|
||||||
if (value === undefined) return false;
|
|
||||||
switch (condition.operator) {
|
|
||||||
case 'gt': return value > condition.value;
|
|
||||||
case 'lt': return value < condition.value;
|
|
||||||
case 'gte': return value >= condition.value;
|
|
||||||
case 'lte': return value <= condition.value;
|
|
||||||
case 'eq': return value === condition.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNestedValue(obj: object, path: string): number | undefined {
|
|
||||||
const parts = path.split('.');
|
|
||||||
let current: unknown = obj;
|
|
||||||
for (const part of parts) {
|
|
||||||
if (current == null || typeof current !== 'object') return undefined;
|
|
||||||
current = (current as Record<string, unknown>)[part];
|
|
||||||
}
|
|
||||||
return typeof current === 'number' ? current : undefined;
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { GameState, EventDefinition, AchievementDefinition } from '@ai-tycoon/shared';
|
import type { GameState, AchievementDefinition } from '@ai-tycoon/shared';
|
||||||
import { processEconomy } from './systems/economySystem';
|
import { processEconomy } from './systems/economySystem';
|
||||||
import { processInfrastructure } from './systems/infrastructureSystem';
|
import { processInfrastructure } from './systems/infrastructureSystem';
|
||||||
import { processCompute } from './systems/computeSystem';
|
import { processCompute } from './systems/computeSystem';
|
||||||
@@ -7,7 +7,6 @@ import { processModels } from './systems/modelSystem';
|
|||||||
import { processMarket } from './systems/marketSystem';
|
import { processMarket } from './systems/marketSystem';
|
||||||
import { processReputation } from './systems/reputationSystem';
|
import { processReputation } from './systems/reputationSystem';
|
||||||
import { processTalent } from './systems/talentSystem';
|
import { processTalent } from './systems/talentSystem';
|
||||||
import { processEvents } from './systems/eventSystem';
|
|
||||||
import { processCompetitors } from './systems/competitorSystem';
|
import { processCompetitors } from './systems/competitorSystem';
|
||||||
import { processData } from './systems/dataSystem';
|
import { processData } from './systems/dataSystem';
|
||||||
import { checkEraTransition } from './systems/eraSystem';
|
import { checkEraTransition } from './systems/eraSystem';
|
||||||
@@ -25,13 +24,8 @@ export interface TickNotification {
|
|||||||
type: 'info' | 'success' | 'warning' | 'danger';
|
type: 'info' | 'success' | 'warning' | 'danger';
|
||||||
}
|
}
|
||||||
|
|
||||||
let cachedEventDefs: EventDefinition[] | null = null;
|
|
||||||
let cachedAchievementDefs: AchievementDefinition[] | null = null;
|
let cachedAchievementDefs: AchievementDefinition[] | null = null;
|
||||||
|
|
||||||
export function setEventDefinitions(defs: EventDefinition[]) {
|
|
||||||
cachedEventDefs = defs;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setAchievementDefinitions(defs: AchievementDefinition[]) {
|
export function setAchievementDefinitions(defs: AchievementDefinition[]) {
|
||||||
cachedAchievementDefs = defs;
|
cachedAchievementDefs = defs;
|
||||||
}
|
}
|
||||||
@@ -88,18 +82,6 @@ export function processTick(state: GameState): Partial<GameState> {
|
|||||||
const data = processData(stateWithTalent);
|
const data = processData(stateWithTalent);
|
||||||
const competitors = processCompetitors(stateWithTalent);
|
const competitors = processCompetitors(stateWithTalent);
|
||||||
|
|
||||||
const eventResult = cachedEventDefs
|
|
||||||
? processEvents(stateWithTalent, cachedEventDefs)
|
|
||||||
: { events: state.events, newEvents: [] };
|
|
||||||
|
|
||||||
for (const evt of eventResult.newEvents) {
|
|
||||||
notifications.push({
|
|
||||||
title: evt.title,
|
|
||||||
message: evt.description,
|
|
||||||
type: evt.category === 'regulatory' ? 'warning' : 'info',
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const tickCount = state.meta.tickCount + 1;
|
const tickCount = state.meta.tickCount + 1;
|
||||||
|
|
||||||
let meta = {
|
let meta = {
|
||||||
@@ -137,7 +119,6 @@ export function processTick(state: GameState): Partial<GameState> {
|
|||||||
reputation,
|
reputation,
|
||||||
data,
|
data,
|
||||||
competitors,
|
competitors,
|
||||||
events: eventResult.events,
|
|
||||||
achievements: state.achievements,
|
achievements: state.achievements,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -165,7 +146,6 @@ export function processTick(state: GameState): Partial<GameState> {
|
|||||||
reputation,
|
reputation,
|
||||||
data,
|
data,
|
||||||
competitors,
|
competitors,
|
||||||
events: eventResult.events,
|
|
||||||
achievements: achievementResult.achievements,
|
achievements: achievementResult.achievements,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export const FAST_FORWARD_BATCH_SIZE = 100;
|
|||||||
export const AUTO_SAVE_INTERVAL_TICKS = 60;
|
export const AUTO_SAVE_INTERVAL_TICKS = 60;
|
||||||
export const FINANCIAL_SNAPSHOT_INTERVAL = 60;
|
export const FINANCIAL_SNAPSHOT_INTERVAL = 60;
|
||||||
export const MAX_FINANCIAL_HISTORY = 1000;
|
export const MAX_FINANCIAL_HISTORY = 1000;
|
||||||
export const MAX_EVENT_HISTORY = 50;
|
|
||||||
export const MAX_REPUTATION_HISTORY = 500;
|
export const MAX_REPUTATION_HISTORY = 500;
|
||||||
|
|
||||||
export const STARTING_MONEY = 50_000;
|
export const STARTING_MONEY = 50_000;
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export * from './types/competitors';
|
|||||||
export * from './types/talent';
|
export * from './types/talent';
|
||||||
export * from './types/data';
|
export * from './types/data';
|
||||||
export * from './types/reputation';
|
export * from './types/reputation';
|
||||||
export * from './types/events';
|
|
||||||
export * from './types/achievements';
|
export * from './types/achievements';
|
||||||
export * from './utils/formatting';
|
export * from './utils/formatting';
|
||||||
export * from './constants/gameBalance';
|
export * from './constants/gameBalance';
|
||||||
|
|||||||
@@ -1,74 +0,0 @@
|
|||||||
import type { Era } from './gameState';
|
|
||||||
|
|
||||||
export interface EventState {
|
|
||||||
activeEvents: ActiveEvent[];
|
|
||||||
eventHistory: EventHistoryEntry[];
|
|
||||||
eventCooldowns: Record<string, number>;
|
|
||||||
eventOccurrences: Record<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ActiveEvent {
|
|
||||||
eventId: string;
|
|
||||||
instanceId: string;
|
|
||||||
triggeredAtTick: number;
|
|
||||||
expiresAtTick: number;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
category: EventCategory;
|
|
||||||
choices: EventChoice[];
|
|
||||||
defaultChoiceIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type EventCategory = 'industry' | 'regulatory' | 'pr' | 'internal' | 'market';
|
|
||||||
|
|
||||||
export interface EventChoice {
|
|
||||||
label: string;
|
|
||||||
description: string;
|
|
||||||
consequences: EventConsequence[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventConsequence {
|
|
||||||
type: 'money' | 'reputation' | 'compute' | 'talent' | 'research_speed'
|
|
||||||
| 'regulation' | 'competitor' | 'unlock' | 'lock' | 'chain_event';
|
|
||||||
value: number;
|
|
||||||
target?: string;
|
|
||||||
delay?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventHistoryEntry {
|
|
||||||
eventId: string;
|
|
||||||
instanceId: string;
|
|
||||||
title: string;
|
|
||||||
category: EventCategory;
|
|
||||||
tick: number;
|
|
||||||
chosenOptionIndex: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventDefinition {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
descriptionTemplate: string;
|
|
||||||
category: EventCategory;
|
|
||||||
eras: Era[];
|
|
||||||
weight: number;
|
|
||||||
cooldownTicks: number;
|
|
||||||
maxOccurrences: number;
|
|
||||||
prerequisites: string[];
|
|
||||||
conditions: EventCondition[];
|
|
||||||
choices: EventChoice[];
|
|
||||||
defaultChoiceIndex: number;
|
|
||||||
expiryTicks: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface EventCondition {
|
|
||||||
field: string;
|
|
||||||
operator: 'gt' | 'lt' | 'gte' | 'lte' | 'eq';
|
|
||||||
value: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const INITIAL_EVENTS: EventState = {
|
|
||||||
activeEvents: [],
|
|
||||||
eventHistory: [],
|
|
||||||
eventCooldowns: {},
|
|
||||||
eventOccurrences: {},
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,6 @@ import type { CompetitorState } from './competitors';
|
|||||||
import type { TalentState } from './talent';
|
import type { TalentState } from './talent';
|
||||||
import type { DataState } from './data';
|
import type { DataState } from './data';
|
||||||
import type { ReputationState } from './reputation';
|
import type { ReputationState } from './reputation';
|
||||||
import type { EventState } from './events';
|
|
||||||
import type { AchievementState } from './achievements';
|
import type { AchievementState } from './achievements';
|
||||||
|
|
||||||
export interface GameState {
|
export interface GameState {
|
||||||
@@ -23,7 +22,6 @@ export interface GameState {
|
|||||||
talent: TalentState;
|
talent: TalentState;
|
||||||
data: DataState;
|
data: DataState;
|
||||||
reputation: ReputationState;
|
reputation: ReputationState;
|
||||||
events: EventState;
|
|
||||||
achievements: AchievementState;
|
achievements: AchievementState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user