diff --git a/apps/web/src/components/game/EventModal.tsx b/apps/web/src/components/game/EventModal.tsx new file mode 100644 index 0000000..8f95119 --- /dev/null +++ b/apps/web/src/components/game/EventModal.tsx @@ -0,0 +1,84 @@ +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 = { + industry: Newspaper, + regulatory: Building2, + pr: Users, + internal: AlertTriangle, + market: TrendingUp, +}; + +const CATEGORY_COLORS: Record = { + 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 ( +
+
+
+
+
+ +

{event.title}

+
+ {event.category} +
+ +

{event.description}

+ +
+ {event.choices.map((choice, idx) => ( + + ))} +
+
+
+
+ ); +} + +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 ( + + {isPositive ? '+' : '-'}{label} + + ); +} diff --git a/apps/web/src/components/layout/MainLayout.tsx b/apps/web/src/components/layout/MainLayout.tsx index 54f25eb..5b76928 100644 --- a/apps/web/src/components/layout/MainLayout.tsx +++ b/apps/web/src/components/layout/MainLayout.tsx @@ -1,13 +1,18 @@ import { Sidebar } from './Sidebar'; import { TopBar } from './TopBar'; import { ToastContainer } from '@/components/common/ToastContainer'; +import { EventModal } from '@/components/game/EventModal'; import { useGameStore } from '@/store'; import { DashboardPage } from '@/pages/DashboardPage'; import { InfrastructurePage } from '@/pages/InfrastructurePage'; +import { ResearchPage } from '@/pages/ResearchPage'; import { ModelsPage } from '@/pages/ModelsPage'; import { SettingsPage } from '@/pages/SettingsPage'; import { MarketPage } from '@/pages/MarketPage'; import { FinancePage } from '@/pages/FinancePage'; +import { TalentPage } from '@/pages/TalentPage'; +import { DataPage } from '@/pages/DataPage'; +import { CompetitorsPage } from '@/pages/CompetitorsPage'; export function MainLayout() { const activePage = useGameStore((s) => s.activePage); @@ -22,6 +27,7 @@ export function MainLayout() { + ); } @@ -30,9 +36,13 @@ function PageRouter({ page }: { page: string }) { switch (page) { case 'dashboard': return ; case 'infrastructure': return ; + case 'research': return ; case 'models': return ; case 'market': return ; case 'finance': return ; + case 'talent': return ; + case 'data': return ; + case 'competitors': return ; case 'settings': return ; default: return ; } diff --git a/apps/web/src/hooks/useGameLoop.ts b/apps/web/src/hooks/useGameLoop.ts index 1c40004..efed17a 100644 --- a/apps/web/src/hooks/useGameLoop.ts +++ b/apps/web/src/hooks/useGameLoop.ts @@ -1,5 +1,5 @@ import { useEffect, useRef } from 'react'; -import { GameEngine } from '@ai-tycoon/game-engine'; +import { GameEngine, setEventDefinitions, EVENT_DEFINITIONS } from '@ai-tycoon/game-engine'; import type { TickNotification } from '@ai-tycoon/game-engine'; import { useGameStore } from '@/store'; @@ -11,6 +11,8 @@ export function useGameLoop(skip = false) { useEffect(() => { if (!companyName || skip) return; + setEventDefinitions(EVENT_DEFINITIONS); + const engine = new GameEngine({ getState: () => { const state = useGameStore.getState(); diff --git a/apps/web/src/pages/CompetitorsPage.tsx b/apps/web/src/pages/CompetitorsPage.tsx new file mode 100644 index 0000000..27409c5 --- /dev/null +++ b/apps/web/src/pages/CompetitorsPage.tsx @@ -0,0 +1,134 @@ +import { Swords, TrendingUp, Shield, Users, Brain } from 'lucide-react'; +import { useGameStore } from '@/store'; +import { formatMoney, formatNumber } from '@ai-tycoon/shared'; + +const ARCHETYPE_LABELS: Record = { + 'safety-first': 'Safety-First Lab', + 'move-fast': 'Move-Fast Startup', + 'big-tech': 'Big Tech Giant', + 'open-source': 'Open Source Maximalist', + 'stealth-startup': 'Stealth Startup', +}; + +const ARCHETYPE_COLORS: Record = { + 'safety-first': 'text-green-400', + 'move-fast': 'text-red-400', + 'big-tech': 'text-blue-400', + 'open-source': 'text-orange-400', + 'stealth-startup': 'text-purple-400', +}; + +export function CompetitorsPage() { + const rivals = useGameStore((s) => s.competitors.rivals); + const industryBenchmark = useGameStore((s) => s.competitors.industryBenchmark); + const playerBest = useGameStore((s) => + s.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0), + ); + + return ( +
+
+

Competitors

+
+ Industry Benchmark: {industryBenchmark.toFixed(1)} +
+
+ +
+

Your Position

+
+
+
Best Model
+
{playerBest.toFixed(1)}/100
+
+
+
+
+ {rivals.filter(r => r.status === 'active').map(rival => ( +
+ ))} +
+
+
+
+ +
+ {rivals.map(rival => ( +
+
+
+

{rival.name}

+ + {ARCHETYPE_LABELS[rival.archetype]} + +
+ + {rival.status} + +
+ +
+ + + + +
+ +
+ {Object.entries(rival.personality).map(([key, val]) => ( +
+
+
+
+
{key.replace(/([A-Z])/g, ' $1').trim()}
+
+ ))} +
+
+ ))} +
+ + {rivals.length === 0 && ( +
+ +

No competitors detected yet.

+
+ )} +
+ ); +} + +function Stat({ icon: Icon, label, value, sub }: { + icon: typeof Brain; + label: string; + value: string; + sub?: string; +}) { + return ( +
+
+ + {label} +
+
{value}
+ {sub &&
{sub}
} +
+ ); +} diff --git a/apps/web/src/pages/DataPage.tsx b/apps/web/src/pages/DataPage.tsx new file mode 100644 index 0000000..a9f75de --- /dev/null +++ b/apps/web/src/pages/DataPage.tsx @@ -0,0 +1,164 @@ +import { useState } from 'react'; +import { Database, ShoppingCart, Zap } from 'lucide-react'; +import { useGameStore } from '@/store'; +import { formatNumber, formatMoney } from '@ai-tycoon/shared'; +import type { OwnedDataset, DataDomain } from '@ai-tycoon/shared'; + +interface MarketplaceDataset { + name: string; + domain: DataDomain; + sizeTokens: number; + quality: number; + legalRisk: number; + price: number; +} + +const MARKETPLACE: MarketplaceDataset[] = [ + { name: 'Wikipedia Dump', domain: 'web', sizeTokens: 5_000_000_000, quality: 0.6, legalRisk: 0.05, price: 5_000 }, + { name: 'GitHub Code Archive', domain: 'code', sizeTokens: 10_000_000_000, quality: 0.5, legalRisk: 0.15, price: 15_000 }, + { name: 'Scientific Papers Bundle', domain: 'scientific', sizeTokens: 3_000_000_000, quality: 0.8, legalRisk: 0.1, price: 20_000 }, + { name: 'Books3 Collection', domain: 'books', sizeTokens: 8_000_000_000, quality: 0.7, legalRisk: 0.6, price: 8_000 }, + { name: 'Reddit Conversations', domain: 'conversation', sizeTokens: 15_000_000_000, quality: 0.3, legalRisk: 0.3, price: 10_000 }, + { name: 'Multilingual Web Crawl', domain: 'multilingual', sizeTokens: 20_000_000_000, quality: 0.4, legalRisk: 0.2, price: 25_000 }, + { name: 'Synthetic Instruction Set', domain: 'synthetic', sizeTokens: 2_000_000_000, quality: 0.9, legalRisk: 0, price: 30_000 }, + { name: 'Image-Caption Pairs', domain: 'images', sizeTokens: 5_000_000_000, quality: 0.6, legalRisk: 0.25, price: 18_000 }, +]; + +const DOMAIN_COLORS: Record = { + web: 'text-blue-400', + code: 'text-green-400', + scientific: 'text-purple-400', + books: 'text-orange-400', + conversation: 'text-yellow-400', + multilingual: 'text-cyan-400', + synthetic: 'text-pink-400', + images: 'text-indigo-400', + video: 'text-red-400', + audio: 'text-teal-400', +}; + +export function DataPage() { + const ownedDatasets = useGameStore((s) => s.data.ownedDatasets); + const totalTokens = useGameStore((s) => s.data.totalTrainingTokens); + const userDataRate = useGameStore((s) => s.data.userDataGenerationRate); + const money = useGameStore((s) => s.economy.money); + const purchaseDataset = useGameStore((s) => s.purchaseDataset); + const tickCount = useGameStore((s) => s.meta.tickCount); + + const ownedNames = new Set(ownedDatasets.map(d => d.name)); + + const handlePurchase = (item: MarketplaceDataset) => { + const dataset: OwnedDataset = { + id: crypto.randomUUID(), + name: item.name, + domain: item.domain, + sizeTokens: item.sizeTokens, + quality: item.quality, + legalRisk: item.legalRisk, + acquiredAtTick: tickCount, + }; + purchaseDataset(dataset, item.price); + }; + + return ( +
+
+

Data

+
+
+ Total Tokens: {formatNumber(totalTokens)} +
+
+ User Data: +{formatNumber(userDataRate)}/s +
+
+
+ +
+

+ + Owned Datasets +

+ {ownedDatasets.length > 0 ? ( +
+ {ownedDatasets.map(ds => ( +
+
+ {ds.name} + {ds.domain} +
+
+ {formatNumber(ds.sizeTokens)} tokens + Quality: {Math.round(ds.quality * 100)}% + {ds.legalRisk > 0.3 && ( + Risk: {Math.round(ds.legalRisk * 100)}% + )} +
+
+ ))} +
+ ) : ( +

No datasets owned.

+ )} +
+ + {userDataRate > 0 && ( +
+

+ + User Data Flywheel +

+

+ Your product users generate {formatNumber(userDataRate)} tokens + per second of training data. More subscribers = more data = better models. +

+
+ )} + +
+

+ + Data Marketplace +

+
+ {MARKETPLACE.map(item => { + const owned = ownedNames.has(item.name); + return ( +
+
+
+ {item.name} + {item.domain} +
+
+ {formatNumber(item.sizeTokens)} tokens + Quality: {Math.round(item.quality * 100)}% + {item.legalRisk > 0.3 && ( + Legal Risk: {Math.round(item.legalRisk * 100)}% + )} +
+
+
+ {owned ? ( + Owned + ) : ( + + )} +
+
+ ); + })} +
+
+
+ ); +} diff --git a/apps/web/src/pages/ResearchPage.tsx b/apps/web/src/pages/ResearchPage.tsx new file mode 100644 index 0000000..4bc5bc2 --- /dev/null +++ b/apps/web/src/pages/ResearchPage.tsx @@ -0,0 +1,147 @@ +import { FlaskConical, Lock, Check, Play } from 'lucide-react'; +import { useGameStore } from '@/store'; +import { formatDuration, formatPercent, formatNumber } from '@ai-tycoon/shared'; +import { TECH_TREE, getAvailableResearch } from '@ai-tycoon/game-engine'; +import type { ResearchNode } from '@ai-tycoon/shared'; + +const CATEGORY_COLORS: Record = { + generation: 'border-purple-500/50 bg-purple-500/10', + efficiency: 'border-blue-500/50 bg-blue-500/10', + safety: 'border-green-500/50 bg-green-500/10', + specialization: 'border-orange-500/50 bg-orange-500/10', + infrastructure: 'border-cyan-500/50 bg-cyan-500/10', +}; + +const CATEGORY_LABELS: Record = { + generation: 'Model Architecture', + efficiency: 'Efficiency', + safety: 'Safety & Alignment', + specialization: 'Specialization', + infrastructure: 'Infrastructure', +}; + +export function ResearchPage() { + const completedResearch = useGameStore((s) => s.research.completedResearch); + const activeResearch = useGameStore((s) => s.research.activeResearch); + const researchPoints = useGameStore((s) => s.research.researchPoints); + const startResearch = useGameStore((s) => s.startResearch); + const era = useGameStore((s) => s.meta.currentEra); + + const state = useGameStore.getState(); + const available = getAvailableResearch(state); + const availableIds = new Set(available.map(n => n.id)); + + const handleStart = (node: ResearchNode) => { + if (activeResearch) return; + startResearch({ + researchId: node.id, + progressTicks: 0, + totalTicks: node.cost.ticks, + allocatedResearchers: state.talent.departments.research.headcount, + allocatedCompute: node.cost.compute, + }); + }; + + const categories = [...new Set(TECH_TREE.map(n => n.category))]; + + return ( +
+
+

Research & Development

+
+
+ Completed: {completedResearch.length}/{TECH_TREE.length} +
+
+ Research Points: {researchPoints} +
+
+
+ + {activeResearch && ( +
+
+
+ + + {TECH_TREE.find(n => n.id === activeResearch.researchId)?.name ?? activeResearch.researchId} + +
+ + {formatPercent(activeResearch.progressTicks / activeResearch.totalTicks)} complete + +
+
+
+
+
+ ETA: {formatDuration(Math.ceil(activeResearch.totalTicks - activeResearch.progressTicks))} +
+
+ )} + + {categories.map(category => { + const nodes = TECH_TREE.filter(n => n.category === category); + return ( +
+

+ {CATEGORY_LABELS[category] ?? category} +

+
+ {nodes.map(node => { + const isCompleted = completedResearch.includes(node.id); + const isActive = activeResearch?.researchId === node.id; + const isAvailable = availableIds.has(node.id); + const isLocked = !isCompleted && !isActive && !isAvailable; + + return ( +
+
+

{node.name}

+ {isCompleted && } + {isLocked && } +
+

{node.description}

+
+
+ {formatDuration(node.cost.ticks)} · {formatNumber(node.cost.compute)} compute + {node.cost.researchPoints > 0 && ` · ${node.cost.researchPoints} RP`} +
+ {isAvailable && !activeResearch && ( + + )} +
+ {node.prerequisites.length > 0 && isLocked && ( +
+ Requires: {node.prerequisites.map(p => + TECH_TREE.find(n => n.id === p)?.name ?? p + ).join(', ')} +
+ )} +
+ ); + })} +
+
+ ); + })} +
+ ); +} diff --git a/apps/web/src/pages/TalentPage.tsx b/apps/web/src/pages/TalentPage.tsx new file mode 100644 index 0000000..109cfcb --- /dev/null +++ b/apps/web/src/pages/TalentPage.tsx @@ -0,0 +1,185 @@ +import { useState } from 'react'; +import { Users, Plus, Star, Briefcase } from 'lucide-react'; +import { useGameStore } from '@/store'; +import { formatMoney } from '@ai-tycoon/shared'; +import { KEY_HIRE_POOL } from '@ai-tycoon/game-engine'; +import type { DepartmentId } from '@ai-tycoon/shared'; + +const DEPT_LABELS: Record = { + research: 'Research', + engineering: 'Engineering', + operations: 'Operations', + sales: 'Sales', +}; + +const DEPT_DESCRIPTIONS: Record = { + research: 'Improves R&D speed and model quality', + engineering: 'Faster training and better infrastructure', + operations: 'Lower costs and higher uptime', + sales: 'More enterprise contracts and revenue', +}; + +export function TalentPage() { + const departments = useGameStore((s) => s.talent.departments); + const keyHires = useGameStore((s) => s.talent.keyHires); + const totalSalary = useGameStore((s) => s.talent.totalSalaryPerTick); + const money = useGameStore((s) => s.economy.money); + const era = useGameStore((s) => s.meta.currentEra); + const hireDepartment = useGameStore((s) => s.hireDepartment); + + const [showKeyHires, setShowKeyHires] = useState(false); + + const hiringCost = 2000; + const canHire = money >= hiringCost; + + const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi']; + const currentEraIdx = eraOrder.indexOf(era); + const availableKeyHires = KEY_HIRE_POOL.filter(h => { + const hireEraIdx = eraOrder.indexOf(h.requiredEra); + if (hireEraIdx > currentEraIdx) return false; + return !keyHires.some(kh => kh.id === h.id); + }); + + return ( +
+
+

Talent

+
+ Total Salary: {formatMoney(totalSalary)}/s +
+
+ +
+ {Object.entries(departments).map(([id, dept]) => ( +
+
+
+

{DEPT_LABELS[id]}

+

{DEPT_DESCRIPTIONS[id]}

+
+
+
{dept.headcount}
+
employees
+
+
+ +
+ + +
+ +
+ + Budget: {formatMoney(dept.budget)}/mo + + +
+
+ ))} +
+ +
+
+

+ + Key Hires +

+ +
+ + {keyHires.length > 0 && ( +
+ {keyHires.map(hire => ( +
+
+
{hire.name}
+
+ {DEPT_LABELS[hire.department]} · {hire.specialAbility} +
+
+
+ {formatMoney(hire.salary)}/s +
+
+ ))} +
+ )} + + {showKeyHires && ( +
+ {availableKeyHires.length > 0 ? availableKeyHires.map(hire => ( +
+
+
{hire.name}
+
{hire.description}
+
+ {DEPT_LABELS[hire.department]} · {formatMoney(hire.salary)}/s +
+
+ +
+ )) : ( +

No key hires available in current era

+ )} +
+ )} + + {keyHires.length === 0 && !showKeyHires && ( +

No key hires recruited yet.

+ )} +
+
+ ); +} + +function StatBar({ label, value }: { label: string; value: number }) { + return ( +
+ {label} +
+
0.7 ? 'bg-success' : value > 0.4 ? 'bg-warning' : 'bg-danger'}`} + style={{ width: `${value * 100}%` }} + /> +
+ {Math.round(value * 100)}% +
+ ); +} diff --git a/apps/web/src/store/index.ts b/apps/web/src/store/index.ts index 07dcc78..d79ebd3 100644 --- a/apps/web/src/store/index.ts +++ b/apps/web/src/store/index.ts @@ -7,6 +7,7 @@ import type { CompetitorState, TalentState, DataState, ReputationState, EventState, AchievementState, DataCenter, GpuType, GpuInventory, TrainingJob, + ActiveResearch, EventConsequence, OwnedDataset, } from '@ai-tycoon/shared'; import { INITIAL_SETTINGS, SAVE_VERSION, @@ -16,6 +17,7 @@ import { INITIAL_REPUTATION, INITIAL_EVENTS, INITIAL_ACHIEVEMENTS, GPU_CONFIGS, } from '@ai-tycoon/shared'; +import { INITIAL_RIVALS } from '@ai-tycoon/game-engine'; export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models' | 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'settings'; @@ -48,6 +50,10 @@ interface Actions { deployModel: (modelId: string) => void; setProductPricing: (productLineId: string, field: string, value: number) => void; toggleProductLine: (productLineId: string) => void; + startResearch: (research: ActiveResearch) => void; + resolveEvent: (instanceId: string, choiceIndex: number) => void; + hireDepartment: (departmentId: string, count: number) => void; + purchaseDataset: (dataset: OwnedDataset, cost: number) => void; updateState: (partial: Partial) => void; } @@ -111,6 +117,10 @@ export const useGameStore = create()( createdAt: Date.now(), lastTickTimestamp: Date.now(), }, + competitors: { + rivals: INITIAL_RIVALS, + industryBenchmark: 0, + }, activePage: 'dashboard', notifications: [], }), @@ -218,6 +228,84 @@ export const useGameStore = create()( }, })), + startResearch: (research) => set((s) => { + if (s.research.activeResearch) return s; + return { + research: { ...s.research, activeResearch: research }, + }; + }), + + 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 }; + 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; + } + } + + return { + economy: { ...s.economy, money: Math.max(0, money) }, + reputation, + 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) => { + const costPerHire = 2000; + const totalCost = costPerHire * count; + if (s.economy.money < totalCost) return s; + + return { + economy: { ...s.economy, money: s.economy.money - totalCost }, + talent: { + ...s.talent, + departments: { + ...s.talent.departments, + [departmentId]: { + ...s.talent.departments[departmentId as keyof typeof s.talent.departments], + headcount: s.talent.departments[departmentId as keyof typeof s.talent.departments].headcount + count, + }, + }, + }, + }; + }), + + purchaseDataset: (dataset, cost) => set((s) => { + if (s.economy.money < cost) return s; + return { + economy: { ...s.economy, money: s.economy.money - cost }, + data: { + ...s.data, + ownedDatasets: [...s.data.ownedDatasets, dataset], + totalTrainingTokens: s.data.totalTrainingTokens + dataset.sizeTokens, + }, + }; + }), + updateState: (partial) => set((s) => { const newState: Partial = {}; for (const key of Object.keys(partial) as (keyof GameState)[]) { diff --git a/packages/game-engine/src/data/competitors.ts b/packages/game-engine/src/data/competitors.ts new file mode 100644 index 0000000..76d5cca --- /dev/null +++ b/packages/game-engine/src/data/competitors.ts @@ -0,0 +1,76 @@ +import type { Competitor } from '@ai-tycoon/shared'; + +/** + * Initial rival AI companies that compete with the player from the start. + * Names are fictional parodies -- any resemblance to real companies is purely satirical. + */ +export const INITIAL_RIVALS: Competitor[] = [ + // ── Safety-first lab (Anthropic parody) ────────────────────────────── + { + id: 'competitor_prometheus', + name: 'Prometheus AI', + archetype: 'safety-first', + personality: { + aggression: 0.2, + safetyFocus: 0.95, + openSourceTendency: 0.3, + marketingFocus: 0.25, + researchFocus: 0.85, + riskTolerance: 0.15, + }, + status: 'active', + estimatedCapability: 18, + estimatedRevenue: 50, + estimatedUsers: 1_200, + reputation: 70, + latestModelName: 'Aegis-1', + completedMilestones: [], + nextMilestoneAtTick: 300, + }, + + // ── Move-fast startup (xAI / Musk parody) ──────────────────────────── + { + id: 'competitor_nexus', + name: 'Nexus Labs', + archetype: 'move-fast', + personality: { + aggression: 0.85, + safetyFocus: 0.15, + openSourceTendency: 0.4, + marketingFocus: 0.7, + researchFocus: 0.6, + riskTolerance: 0.9, + }, + status: 'active', + estimatedCapability: 14, + estimatedRevenue: 30, + estimatedUsers: 3_500, + reputation: 45, + latestModelName: 'Blitz-0.9', + completedMilestones: [], + nextMilestoneAtTick: 300, + }, + + // ── Big-tech giant (Google parody) ──────────────────────────────────── + { + id: 'competitor_titan', + name: 'Titan Computing', + archetype: 'big-tech', + personality: { + aggression: 0.5, + safetyFocus: 0.5, + openSourceTendency: 0.35, + marketingFocus: 0.55, + researchFocus: 0.65, + riskTolerance: 0.4, + }, + status: 'active', + estimatedCapability: 22, + estimatedRevenue: 200, + estimatedUsers: 15_000, + reputation: 60, + latestModelName: 'Colossus 2.0', + completedMilestones: [], + nextMilestoneAtTick: 300, + }, +]; diff --git a/packages/game-engine/src/data/events.ts b/packages/game-engine/src/data/events.ts new file mode 100644 index 0000000..ba5f269 --- /dev/null +++ b/packages/game-engine/src/data/events.ts @@ -0,0 +1,1562 @@ +import type { EventDefinition } from '@ai-tycoon/shared'; + +export const EVENT_DEFINITIONS: EventDefinition[] = [ + // ============================================================ + // INDUSTRY EVENTS (8) + // ============================================================ + { + id: 'industry_gpu_shortage', + title: 'GPU Shortage Hits the Market', + descriptionTemplate: + 'NVIDIA announces allocation limits on H100s. Every AI lab is scrambling for compute. Your procurement team needs direction.', + category: 'industry', + eras: ['startup', 'scaleup', 'bigtech'], + weight: 8, + cooldownTicks: 900, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Panic-buy at scalper prices', + description: + 'Secure GPUs now at 3x markup before the shortage gets worse.', + consequences: [ + { type: 'money', value: -80000 }, + { type: 'compute', value: 50 }, + ], + }, + { + label: 'Negotiate a cloud provider deal', + description: + 'Lock in a long-term cloud contract for reserved instances instead of buying hardware.', + consequences: [ + { type: 'money', value: -40000 }, + { type: 'compute', value: 25 }, + { type: 'reputation', value: 3 }, + ], + }, + { + label: 'Optimize existing infrastructure', + description: + 'Invest engineering time into squeezing more out of what you have.', + consequences: [ + { type: 'money', value: -10000 }, + { type: 'research_speed', value: 0.15 }, + { type: 'talent', value: -1 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 300, + }, + { + id: 'industry_new_benchmark', + title: 'New AI Benchmark Released', + descriptionTemplate: + 'Researchers at Stanford publish "MegaEval-2000", a comprehensive benchmark that makes existing evals look like kindergarten quizzes. The industry is racing to top the leaderboard.', + category: 'industry', + eras: ['startup', 'scaleup', 'bigtech', 'agi'], + weight: 7, + cooldownTicks: 600, + maxOccurrences: 3, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Prioritize benchmark performance', + description: + 'Redirect research to chase leaderboard glory. Great for marketing, questionable for real progress.', + consequences: [ + { type: 'reputation', value: 10 }, + { type: 'research_speed', value: -0.1 }, + { type: 'money', value: -15000 }, + ], + }, + { + label: 'Ignore the hype', + description: + 'Stay focused on your own research agenda. The benchmark will be irrelevant in six months anyway.', + consequences: [ + { type: 'research_speed', value: 0.1 }, + { type: 'reputation', value: -3 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 240, + }, + { + id: 'industry_ai_winter_scare', + title: 'AI Winter Scare', + descriptionTemplate: + 'A prominent AI researcher publishes a paper titled "Scaling Laws Have Hit a Wall." Tech Twitter melts down. VCs start sweating. Your investors are calling.', + category: 'industry', + eras: ['scaleup', 'bigtech'], + weight: 5, + cooldownTicks: 1800, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'economy.money', operator: 'gte', value: 50000 }, + ], + choices: [ + { + label: 'Double down publicly', + description: + 'Publish a bold rebuttal and announce expanded research plans. High risk, high reward.', + consequences: [ + { type: 'money', value: -60000 }, + { type: 'reputation', value: 12 }, + { type: 'research_speed', value: 0.2 }, + ], + }, + { + label: 'Quietly diversify', + description: + 'Hedge by pivoting some resources toward applied AI products with near-term revenue.', + consequences: [ + { type: 'money', value: 30000 }, + { type: 'reputation', value: -2 }, + { type: 'research_speed', value: -0.1 }, + ], + }, + { + label: 'Stay the course', + description: + 'Issue a measured statement and keep doing what you are doing. Boring but stable.', + consequences: [ + { type: 'reputation', value: 2 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 480, + }, + { + id: 'industry_opensource_breakthrough', + title: 'Open-Source Breakthrough by Competitor', + descriptionTemplate: + 'MetaBook AI just dropped "LLaMA-Next" with Apache 2.0 licensing. It matches your best model on most benchmarks. Reddit is celebrating. Your sales team is panicking.', + category: 'industry', + eras: ['scaleup', 'bigtech'], + weight: 6, + cooldownTicks: 1200, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Open-source your own model', + description: + 'If you can not beat them, join them. Release your model weights and become the community darling.', + consequences: [ + { type: 'money', value: -30000 }, + { type: 'reputation', value: 15 }, + { type: 'research_speed', value: 0.1 }, + ], + }, + { + label: 'Emphasize proprietary advantages', + description: + 'Double down on enterprise features, safety, and reliability that open-source cannot match.', + consequences: [ + { type: 'money', value: 20000 }, + { type: 'reputation', value: -5 }, + ], + }, + { + label: 'Build on top of their model', + description: + 'Use the open-source model as a base and fine-tune it. Pragmatic, if a bit shameless.', + consequences: [ + { type: 'money', value: -5000 }, + { type: 'compute', value: -10 }, + { type: 'research_speed', value: 0.2 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 360, + }, + { + id: 'industry_cloud_partnership', + title: 'Cloud Provider Partnership Offer', + descriptionTemplate: + 'Amazure Web Services reaches out about a strategic partnership. They will give you discounted compute in exchange for making your models available on their platform exclusively.', + category: 'industry', + eras: ['startup', 'scaleup'], + weight: 6, + cooldownTicks: 1200, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'reputation.score', operator: 'gte', value: 20 }, + ], + choices: [ + { + label: 'Accept the exclusive deal', + description: + 'Cheap compute now, vendor lock-in later. A Faustian bargain wrapped in a term sheet.', + consequences: [ + { type: 'money', value: 100000 }, + { type: 'compute', value: 80 }, + { type: 'reputation', value: -5 }, + ], + }, + { + label: 'Counter with a non-exclusive offer', + description: + 'Negotiate a preferred-partner deal while keeping your options open.', + consequences: [ + { type: 'money', value: 40000 }, + { type: 'compute', value: 30 }, + { type: 'reputation', value: 3 }, + ], + }, + { + label: 'Decline politely', + description: + 'Maintain full independence. Your infra team will figure it out.', + consequences: [ + { type: 'reputation', value: 5 }, + { type: 'money', value: -10000 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 480, + }, + { + id: 'industry_hardware_price_crash', + title: 'Hardware Price Crash', + descriptionTemplate: + 'A new chip fab comes online in Taiwan, flooding the market with GPUs. Prices drop 40% overnight. Time to go shopping.', + category: 'industry', + eras: ['startup', 'scaleup', 'bigtech'], + weight: 4, + cooldownTicks: 1500, + maxOccurrences: 1, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Buy aggressively', + description: + 'Stock up while prices are low. Fill every rack.', + consequences: [ + { type: 'money', value: -50000 }, + { type: 'compute', value: 100 }, + ], + }, + { + label: 'Buy modestly', + description: + 'Pick up some extra capacity at the discount, but do not overextend.', + consequences: [ + { type: 'money', value: -20000 }, + { type: 'compute', value: 40 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 240, + }, + { + id: 'industry_power_outage', + title: 'Data Center Power Outage', + descriptionTemplate: + 'A freak ice storm knocks out power to your primary data center in Oregon. Training runs are interrupted. Customers are experiencing downtime.', + category: 'industry', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 5, + cooldownTicks: 900, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Emergency failover to backup region', + description: + 'Spin up in your secondary region immediately. Expensive but fast.', + consequences: [ + { type: 'money', value: -35000 }, + { type: 'reputation', value: 5 }, + ], + }, + { + label: 'Wait for power restoration', + description: + 'The power company says 12-24 hours. Probably fine. Probably.', + consequences: [ + { type: 'money', value: -5000 }, + { type: 'reputation', value: -10 }, + { type: 'compute', value: -20 }, + ], + }, + { + label: 'Invest in redundancy for next time', + description: + 'Fix the immediate issue and build out multi-region redundancy so this never happens again.', + consequences: [ + { type: 'money', value: -80000 }, + { type: 'reputation', value: 3 }, + { type: 'compute', value: 30 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 120, + }, + { + id: 'industry_chip_export_controls', + title: 'Chip Export Controls Tightened', + descriptionTemplate: + 'The Department of Commerce announces new export restrictions on advanced AI chips. Your supply chain team is evaluating the impact.', + category: 'industry', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 5, + cooldownTicks: 1500, + maxOccurrences: 1, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Lobby for exemptions', + description: + 'Hire lobbyists and argue your chips are for "research purposes only." Classic.', + consequences: [ + { type: 'money', value: -40000 }, + { type: 'regulation', value: 5 }, + { type: 'reputation', value: -3 }, + ], + }, + { + label: 'Invest in domestic alternatives', + description: + 'Start working with US-based chip startups. More expensive but politically safer.', + consequences: [ + { type: 'money', value: -50000 }, + { type: 'compute', value: 20 }, + { type: 'reputation', value: 5 }, + ], + }, + { + label: 'Adapt training to lower-end hardware', + description: + 'Invest in algorithmic efficiency to work with whatever chips are available.', + consequences: [ + { type: 'money', value: -15000 }, + { type: 'research_speed', value: 0.15 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 480, + }, + + // ============================================================ + // REGULATORY EVENTS (6) + // ============================================================ + { + id: 'regulatory_congressional_hearing', + title: 'Congressional Hearing on AI', + descriptionTemplate: + 'You have been invited to testify before the Senate Commerce Committee on "The Promises and Perils of Artificial Intelligence." Senator from Nebraska wants to know if your AI can feel pain.', + category: 'regulatory', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 6, + cooldownTicks: 1200, + maxOccurrences: 2, + prerequisites: [], + conditions: [ + { field: 'reputation.score', operator: 'gte', value: 30 }, + ], + choices: [ + { + label: 'Send the CEO with a charm offensive', + description: + 'Wear a hoodie, speak in platitudes about "responsible innovation," promise self-regulation.', + consequences: [ + { type: 'reputation', value: 8 }, + { type: 'regulation', value: 3 }, + { type: 'money', value: -10000 }, + ], + }, + { + label: 'Send the legal team', + description: + 'Play it safe with carefully prepared statements that say absolutely nothing.', + consequences: [ + { type: 'reputation', value: 2 }, + { type: 'money', value: -20000 }, + ], + }, + { + label: 'Decline to testify', + description: + 'You are too busy building the future. Congress can wait.', + consequences: [ + { type: 'reputation', value: -8 }, + { type: 'regulation', value: -5 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 360, + }, + { + id: 'regulatory_eu_ai_act', + title: 'EU AI Act Compliance Deadline', + descriptionTemplate: + 'The EU AI Act classifies your flagship model as "high-risk." You have 90 days to comply or face fines up to 6% of global revenue. The compliance checklist is 47 pages long.', + category: 'regulatory', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 7, + cooldownTicks: 1800, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'economy.money', operator: 'gte', value: 100000 }, + ], + choices: [ + { + label: 'Full compliance effort', + description: + 'Hire a compliance team and do everything by the book. Expensive but opens the EU market.', + consequences: [ + { type: 'money', value: -80000 }, + { type: 'reputation', value: 10 }, + { type: 'regulation', value: 10 }, + { type: 'talent', value: -1 }, + ], + }, + { + label: 'Minimal compliance', + description: + 'Do the bare minimum. Check the boxes, ship the paperwork, hope for the best.', + consequences: [ + { type: 'money', value: -25000 }, + { type: 'reputation', value: -2 }, + { type: 'regulation', value: 3 }, + ], + }, + { + label: 'Exit the EU market', + description: + 'Pull out of Europe entirely. American AI for American customers.', + consequences: [ + { type: 'money', value: -40000 }, + { type: 'reputation', value: -10 }, + { type: 'regulation', value: -3 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 600, + }, + { + id: 'regulatory_safety_audit', + title: 'Safety Audit Demanded', + descriptionTemplate: + 'A coalition of AI safety organizations publishes an open letter demanding an independent audit of your model. #AuditTheirAI is trending on Twitter.', + category: 'regulatory', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 6, + cooldownTicks: 900, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Welcome the audit', + description: + 'Invite auditors in, publish the results transparently. Nothing to hide here.', + consequences: [ + { type: 'money', value: -30000 }, + { type: 'reputation', value: 12 }, + { type: 'regulation', value: 8 }, + { type: 'research_speed', value: -0.1 }, + ], + }, + { + label: 'Conduct an internal audit instead', + description: + 'We will audit ourselves, thanks. Publish a glossy safety report.', + consequences: [ + { type: 'money', value: -15000 }, + { type: 'reputation', value: -3 }, + { type: 'regulation', value: 2 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 360, + }, + { + id: 'regulatory_privacy_lawsuit', + title: 'Data Privacy Lawsuit', + descriptionTemplate: + 'A class-action lawsuit alleges your model was trained on personal data without consent. The plaintiffs\' lawyers are asking for $500M. Your legal team says the actual exposure is much lower, but the headlines are brutal.', + category: 'regulatory', + eras: ['scaleup', 'bigtech'], + weight: 5, + cooldownTicks: 1500, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'economy.money', operator: 'gte', value: 80000 }, + ], + choices: [ + { + label: 'Settle quickly', + description: + 'Pay to make it go away before the headlines get worse.', + consequences: [ + { type: 'money', value: -100000 }, + { type: 'reputation', value: -5 }, + ], + }, + { + label: 'Fight it in court', + description: + 'Hire top litigators and contest every claim. Could take years but sets important precedent.', + consequences: [ + { type: 'money', value: -50000 }, + { type: 'reputation', value: -8 }, + { type: 'regulation', value: 5 }, + ], + }, + { + label: 'Settle and announce new data practices', + description: + 'Settle the lawsuit and make a big show of implementing new privacy standards.', + consequences: [ + { type: 'money', value: -80000 }, + { type: 'reputation', value: 5 }, + { type: 'regulation', value: 8 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 480, + }, + { + id: 'regulatory_lobbying_opportunity', + title: 'Lobbying Opportunity', + descriptionTemplate: + 'A new AI regulation bill is being drafted. A well-connected DC lobbying firm offers to represent your interests. They know the right people.', + category: 'regulatory', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 5, + cooldownTicks: 900, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Hire the lobbyists', + description: + 'Play the game. Everyone else in the industry already has lobbyists.', + consequences: [ + { type: 'money', value: -45000 }, + { type: 'regulation', value: 10 }, + { type: 'reputation', value: -5 }, + ], + }, + { + label: 'Engage through industry associations', + description: + 'Work through existing industry groups for a more collaborative approach.', + consequences: [ + { type: 'money', value: -15000 }, + { type: 'regulation', value: 5 }, + { type: 'reputation', value: 2 }, + ], + }, + { + label: 'Stay out of politics', + description: + 'Focus on building great technology and let the chips fall where they may.', + consequences: [ + { type: 'reputation', value: 3 }, + { type: 'regulation', value: -3 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 360, + }, + { + id: 'regulatory_sandbox_approval', + title: 'Regulatory Sandbox Approval', + descriptionTemplate: + 'Your application to the UK AI Regulatory Sandbox has been approved. You can deploy experimental models with reduced regulatory burden for 12 months.', + category: 'regulatory', + eras: ['startup', 'scaleup'], + weight: 4, + cooldownTicks: 1500, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'reputation.score', operator: 'gte', value: 15 }, + ], + choices: [ + { + label: 'Go all-in on experimentation', + description: + 'Use the sandbox to test your most aggressive models. Move fast, learn fast.', + consequences: [ + { type: 'research_speed', value: 0.3 }, + { type: 'reputation', value: 5 }, + { type: 'regulation', value: 3 }, + ], + }, + { + label: 'Use it conservatively', + description: + 'Test incrementally and build a track record of responsible innovation.', + consequences: [ + { type: 'research_speed', value: 0.15 }, + { type: 'reputation', value: 8 }, + { type: 'regulation', value: 6 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 480, + }, + + // ============================================================ + // PR / CULTURAL EVENTS (6) + // ============================================================ + { + id: 'pr_viral_meme', + title: 'Viral AI Meme', + descriptionTemplate: + 'Someone discovers your chatbot gives hilariously unhinged responses when asked about medieval siege warfare. The screenshots go mega-viral. "CatapultGPT" is the top trending topic.', + category: 'pr', + eras: ['startup', 'scaleup', 'bigtech'], + weight: 7, + cooldownTicks: 600, + maxOccurrences: 3, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Lean into it', + description: + 'Post memes from the official account. Embrace the chaos. This is marketing you could never buy.', + consequences: [ + { type: 'reputation', value: 10 }, + { type: 'money', value: 15000 }, + ], + }, + { + label: 'Patch the behavior quickly', + description: + 'Fix the model and issue a professional statement about "ongoing improvements."', + consequences: [ + { type: 'reputation', value: -2 }, + { type: 'research_speed', value: -0.05 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 180, + }, + { + id: 'pr_hallucination_viral', + title: 'Hallucination Incident Goes Viral', + descriptionTemplate: + 'Your model confidently told a user that the Eiffel Tower is located in Berlin and was built by Nikola Tesla. The screenshot has 2 million views. Late night comedians are having a field day.', + category: 'pr', + eras: ['startup', 'scaleup', 'bigtech'], + weight: 6, + cooldownTicks: 900, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Issue a transparent postmortem', + description: + 'Acknowledge the problem, explain the technical root cause, detail your plan to fix it.', + consequences: [ + { type: 'reputation', value: 3 }, + { type: 'money', value: -10000 }, + { type: 'research_speed', value: 0.1 }, + ], + }, + { + label: 'Downplay with humor', + description: + 'Tweet "Even AI needs a geography tutor sometimes" and move on.', + consequences: [ + { type: 'reputation', value: -5 }, + { type: 'money', value: 5000 }, + ], + }, + { + label: 'Rush out a hotfix', + description: + 'Emergency patch the model with guardrails specifically for geography questions.', + consequences: [ + { type: 'money', value: -20000 }, + { type: 'reputation', value: 1 }, + { type: 'research_speed', value: -0.1 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 240, + }, + { + id: 'pr_celebrity_endorsement', + title: 'Celebrity Endorsement', + descriptionTemplate: + 'Pop star Arianna Grandioso posts an Instagram story raving about how she uses your AI to write song lyrics. 80 million followers just learned your name.', + category: 'pr', + eras: ['startup', 'scaleup', 'bigtech'], + weight: 4, + cooldownTicks: 1200, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'reputation.score', operator: 'gte', value: 10 }, + ], + choices: [ + { + label: 'Offer a sponsorship deal', + description: + 'Strike while the iron is hot. Pay for an official partnership.', + consequences: [ + { type: 'money', value: -60000 }, + { type: 'reputation', value: 15 }, + ], + }, + { + label: 'Send a thank-you and free API credits', + description: + 'A classy response that keeps the organic feel without breaking the bank.', + consequences: [ + { type: 'money', value: -5000 }, + { type: 'reputation', value: 8 }, + ], + }, + { + label: 'Ignore it', + description: + 'Celebrity endorsements are fleeting. Stay focused on the product.', + consequences: [ + { type: 'reputation', value: 3 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 300, + }, + { + id: 'pr_ai_art_controversy', + title: 'AI Art Controversy', + descriptionTemplate: + 'An AI-generated image made with your model wins a major art competition. Artists are furious. #BanAIArt is trending. The winning piece is admittedly pretty good though.', + category: 'pr', + eras: ['startup', 'scaleup', 'bigtech'], + weight: 5, + cooldownTicks: 1200, + maxOccurrences: 1, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Side with the artists', + description: + 'Announce new content provenance tools and commit to compensating artists whose work was used in training.', + consequences: [ + { type: 'money', value: -40000 }, + { type: 'reputation', value: 8 }, + { type: 'research_speed', value: -0.1 }, + ], + }, + { + label: 'Defend AI creativity', + description: + 'Publish a blog post arguing that AI art is a new medium, not theft. Bold stance.', + consequences: [ + { type: 'reputation', value: -5 }, + { type: 'money', value: 10000 }, + { type: 'research_speed', value: 0.05 }, + ], + }, + { + label: 'Stay neutral', + description: + 'Release a measured statement about the "evolving relationship between AI and creativity."', + consequences: [ + { type: 'reputation', value: -1 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 360, + }, + { + id: 'pr_employee_leak', + title: 'Employee Leaks Model Weights', + descriptionTemplate: + 'A disgruntled former employee uploads your latest model weights to a torrent site. By the time legal gets involved, 50,000 people have downloaded them. Hugging Face already has three fine-tunes.', + category: 'pr', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 4, + cooldownTicks: 1500, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'economy.money', operator: 'gte', value: 60000 }, + ], + choices: [ + { + label: 'Embrace it as unplanned open-source', + description: + 'Spin the narrative. "We were going to open-source it anyway!" You were not.', + consequences: [ + { type: 'reputation', value: 5 }, + { type: 'money', value: -20000 }, + { type: 'research_speed', value: 0.1 }, + ], + }, + { + label: 'Pursue legal action', + description: + 'DMCA takedowns, lawsuit against the leaker, the whole nine yards.', + consequences: [ + { type: 'money', value: -50000 }, + { type: 'reputation', value: -8 }, + { type: 'regulation', value: 3 }, + ], + }, + { + label: 'Tighten security and move on', + description: + 'Invest in better internal security. What is leaked is leaked.', + consequences: [ + { type: 'money', value: -30000 }, + { type: 'reputation', value: -3 }, + { type: 'talent', value: -1 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 240, + }, + { + id: 'pr_ai_bestseller', + title: 'AI Writes a Bestselling Novel', + descriptionTemplate: + 'A user publishes a novel written entirely with your model. It hits the NYT bestseller list. The literary world is divided. Oprah wants to do a special episode.', + category: 'pr', + eras: ['scaleup', 'bigtech'], + weight: 3, + cooldownTicks: 1800, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'reputation.score', operator: 'gte', value: 25 }, + ], + choices: [ + { + label: 'Celebrate publicly', + description: + 'Tout this as a milestone for AI creativity. Great for brand awareness.', + consequences: [ + { type: 'reputation', value: 12 }, + { type: 'money', value: 25000 }, + ], + }, + { + label: 'Distance yourself', + description: + 'Emphasize that the user is the creative force, not the tool. Authors already hate you enough.', + consequences: [ + { type: 'reputation', value: 3 }, + ], + }, + { + label: 'Launch a creative writing tier', + description: + 'Use the publicity to launch a premium creative writing subscription.', + consequences: [ + { type: 'money', value: 50000 }, + { type: 'reputation', value: 5 }, + { type: 'talent', value: 1 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 360, + }, + + // ============================================================ + // INTERNAL EVENTS (7) + // ============================================================ + { + id: 'internal_researcher_poaching', + title: 'Key Researcher Poaching Attempt', + descriptionTemplate: + 'Goggle DeepThink is trying to poach your lead alignment researcher with a $2M signing bonus and a promise to "let them do whatever they want." They are seriously considering it.', + category: 'internal', + eras: ['startup', 'scaleup', 'bigtech', 'agi'], + weight: 7, + cooldownTicks: 600, + maxOccurrences: 3, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Counter-offer with equity', + description: + 'Match the salary and throw in a generous equity package. Talent retention is worth it.', + consequences: [ + { type: 'money', value: -40000 }, + { type: 'talent', value: 1 }, + { type: 'research_speed', value: 0.15 }, + ], + }, + { + label: 'Let them go gracefully', + description: + 'Wish them well, maintain the relationship. Good people leave sometimes.', + consequences: [ + { type: 'talent', value: -2 }, + { type: 'research_speed', value: -0.15 }, + { type: 'reputation', value: 2 }, + ], + }, + { + label: 'Promise a leadership role', + description: + 'Promote them to Head of Research. Sometimes a title is worth more than money.', + consequences: [ + { type: 'money', value: -15000 }, + { type: 'talent', value: 1 }, + { type: 'research_speed', value: 0.1 }, + { type: 'reputation', value: 3 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 180, + }, + { + id: 'internal_burnout_wave', + title: 'Engineering Burnout Wave', + descriptionTemplate: + 'After months of 80-hour weeks chasing AGI, your engineering team is running on fumes and energy drinks. Three people called in sick this week. One just posted "thinking about touching grass" on LinkedIn.', + category: 'internal', + eras: ['startup', 'scaleup', 'bigtech', 'agi'], + weight: 7, + cooldownTicks: 800, + maxOccurrences: 3, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Mandatory rest week', + description: + 'Shut down non-critical operations for a week. Everyone recharges.', + consequences: [ + { type: 'research_speed', value: -0.1 }, + { type: 'talent', value: 2 }, + { type: 'money', value: -10000 }, + ], + }, + { + label: 'Hire more engineers to spread the load', + description: + 'Throw money at the problem. More people, less individual burden.', + consequences: [ + { type: 'money', value: -60000 }, + { type: 'talent', value: 3 }, + { type: 'research_speed', value: 0.05 }, + ], + }, + { + label: 'Pizza party and motivational speech', + description: + 'Order some pizzas, give a rousing speech about changing the world. That fixes burnout, right?', + consequences: [ + { type: 'money', value: -500 }, + { type: 'talent', value: -1 }, + { type: 'reputation', value: -2 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 240, + }, + { + id: 'internal_brilliant_intern', + title: 'Brilliant Intern', + descriptionTemplate: + 'Your summer intern from MIT just submitted a PR that improves training efficiency by 23%. The senior engineers are equal parts impressed and threatened.', + category: 'internal', + eras: ['startup', 'scaleup', 'bigtech'], + weight: 5, + cooldownTicks: 1200, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Offer a full-time position immediately', + description: + 'Lock this down before anyone else does. Skip the formalities.', + consequences: [ + { type: 'money', value: -25000 }, + { type: 'talent', value: 2 }, + { type: 'research_speed', value: 0.2 }, + ], + }, + { + label: 'Extend the internship', + description: + 'Keep them for another semester. See if the magic continues.', + consequences: [ + { type: 'money', value: -8000 }, + { type: 'talent', value: 1 }, + { type: 'research_speed', value: 0.1 }, + ], + }, + { + label: 'Pat them on the back and move on', + description: + 'Good work, kid. Now get back to your homework.', + consequences: [ + { type: 'research_speed', value: 0.05 }, + { type: 'reputation', value: -2 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 300, + }, + { + id: 'internal_hackathon', + title: 'Internal Hackathon', + descriptionTemplate: + 'Your team proposes a 48-hour hackathon. Past hackathons have produced some of your best features, but they also produced that chatbot that only speaks in haiku.', + category: 'internal', + eras: ['startup', 'scaleup', 'bigtech'], + weight: 6, + cooldownTicks: 800, + maxOccurrences: 3, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Full company hackathon', + description: + 'Everyone participates. Two days of chaos, Red Bull, and potential breakthroughs.', + consequences: [ + { type: 'money', value: -15000 }, + { type: 'research_speed', value: 0.2 }, + { type: 'talent', value: 1 }, + ], + }, + { + label: 'Small volunteer team only', + description: + 'Let enthusiasts participate while everyone else keeps the lights on.', + consequences: [ + { type: 'money', value: -5000 }, + { type: 'research_speed', value: 0.1 }, + ], + }, + { + label: 'Skip it, we have deadlines', + description: + 'Fun is for companies that are not trying to build AGI.', + consequences: [ + { type: 'talent', value: -1 }, + { type: 'reputation', value: -1 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 240, + }, + { + id: 'internal_security_breach', + title: 'Security Breach Discovered', + descriptionTemplate: + 'Your security team discovers unauthorized access to internal systems. Someone has been exfiltrating training data for weeks. The breach appears to originate from a state-sponsored group.', + category: 'internal', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 4, + cooldownTicks: 1500, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'economy.money', operator: 'gte', value: 50000 }, + ], + choices: [ + { + label: 'Full incident response', + description: + 'Engage a top-tier cybersecurity firm, notify affected parties, do everything by the book.', + consequences: [ + { type: 'money', value: -80000 }, + { type: 'reputation', value: 5 }, + { type: 'regulation', value: 5 }, + { type: 'research_speed', value: -0.15 }, + ], + }, + { + label: 'Handle it quietly internally', + description: + 'Patch the vulnerability, rotate credentials, and hope nobody notices.', + consequences: [ + { type: 'money', value: -20000 }, + { type: 'reputation', value: -10 }, + { type: 'research_speed', value: -0.05 }, + ], + }, + { + label: 'Disclose and collaborate with authorities', + description: + 'Go public and work with the FBI. Maximum transparency, maximum disruption.', + consequences: [ + { type: 'money', value: -50000 }, + { type: 'reputation', value: 10 }, + { type: 'regulation', value: 8 }, + { type: 'research_speed', value: -0.2 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 180, + }, + { + id: 'internal_office_expansion', + title: 'Office Expansion Needed', + descriptionTemplate: + 'You have more employees than desks. The break room has been converted into a workspace. Someone is coding in the supply closet. Time to think about space.', + category: 'internal', + eras: ['startup', 'scaleup'], + weight: 6, + cooldownTicks: 1200, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Lease a fancy new office', + description: + 'Move to a gleaming office with a roof deck and a meditation room. Attract top talent.', + consequences: [ + { type: 'money', value: -100000 }, + { type: 'talent', value: 2 }, + { type: 'reputation', value: 5 }, + ], + }, + { + label: 'Go fully remote', + description: + 'Cancel the lease entirely. Embrace the future of work. Save a fortune.', + consequences: [ + { type: 'money', value: 30000 }, + { type: 'talent', value: 1 }, + { type: 'research_speed', value: -0.05 }, + ], + }, + { + label: 'Find a modest expansion', + description: + 'Lease the floor above you. Practical, not glamorous.', + consequences: [ + { type: 'money', value: -40000 }, + { type: 'talent', value: 1 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 480, + }, + { + id: 'internal_culture_clash', + title: 'Safety vs. Speed Culture Clash', + descriptionTemplate: + 'A heated Slack thread between the safety team and the product team has devolved into a company-wide debate. The safety team wants to delay the next launch by three months. The product team says competitors will eat your lunch.', + category: 'internal', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 6, + cooldownTicks: 900, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Side with safety', + description: + 'Delay the launch. Responsible AI development is the priority.', + consequences: [ + { type: 'research_speed', value: -0.15 }, + { type: 'reputation', value: 8 }, + { type: 'regulation', value: 5 }, + ], + }, + { + label: 'Side with product', + description: + 'Ship it. You can always patch safety issues after launch.', + consequences: [ + { type: 'research_speed', value: 0.15 }, + { type: 'reputation', value: -5 }, + { type: 'money', value: 30000 }, + ], + }, + { + label: 'Find a compromise', + description: + 'Launch with reduced capabilities and a safety roadmap. Nobody is happy, which means it is probably the right call.', + consequences: [ + { type: 'reputation', value: 3 }, + { type: 'money', value: 10000 }, + { type: 'talent', value: -1 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 300, + }, + + // ============================================================ + // MARKET EVENTS (7) + // ============================================================ + { + id: 'market_enterprise_contract', + title: 'Enterprise Contract Offer', + descriptionTemplate: + 'MegaCorp Industries wants to deploy your AI across their 50,000-person organization. The contract is lucrative but they want custom fine-tuning, 99.99% uptime SLA, and a dedicated support team.', + category: 'market', + eras: ['scaleup', 'bigtech'], + weight: 6, + cooldownTicks: 900, + maxOccurrences: 2, + prerequisites: [], + conditions: [ + { field: 'reputation.score', operator: 'gte', value: 20 }, + ], + choices: [ + { + label: 'Accept the full contract', + description: + 'Commit the resources. Enterprise revenue is the path to sustainability.', + consequences: [ + { type: 'money', value: 150000 }, + { type: 'talent', value: -1 }, + { type: 'research_speed', value: -0.1 }, + { type: 'reputation', value: 5 }, + ], + }, + { + label: 'Negotiate reduced scope', + description: + 'Take the contract but with a more realistic SLA and timeline.', + consequences: [ + { type: 'money', value: 80000 }, + { type: 'reputation', value: 3 }, + ], + }, + { + label: 'Decline - stay focused on the API', + description: + 'Enterprise contracts are a distraction from your core mission.', + consequences: [ + { type: 'reputation', value: -3 }, + { type: 'research_speed', value: 0.05 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 360, + }, + { + id: 'market_consumer_surge', + title: 'Consumer Demand Surge', + descriptionTemplate: + 'Your chatbot goes viral on TikTok after someone uses it to plan an entire wedding. Traffic spikes 500% overnight. Your servers are sweating.', + category: 'market', + eras: ['startup', 'scaleup', 'bigtech'], + weight: 6, + cooldownTicks: 800, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Scale up infrastructure immediately', + description: + 'Throw money at servers to handle the surge. Do not let this moment slip.', + consequences: [ + { type: 'money', value: -45000 }, + { type: 'reputation', value: 8 }, + { type: 'compute', value: -30 }, + ], + }, + { + label: 'Implement a waitlist', + description: + 'Create artificial scarcity. Nothing builds hype like exclusivity.', + consequences: [ + { type: 'money', value: 10000 }, + { type: 'reputation', value: 5 }, + ], + }, + { + label: 'Rate-limit aggressively', + description: + 'Keep the servers stable at the cost of user experience. Some visitors will bounce.', + consequences: [ + { type: 'reputation', value: -3 }, + { type: 'money', value: -5000 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 180, + }, + { + id: 'market_api_price_war', + title: 'API Price War', + descriptionTemplate: + 'Competitor "CheapTokens Inc." slashes their API prices by 70%. Your customers are asking pointed questions about your pricing. The race to the bottom has begun.', + category: 'market', + eras: ['scaleup', 'bigtech'], + weight: 7, + cooldownTicks: 1200, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Match their prices', + description: + 'Cut prices to stay competitive. Margins will hurt but you keep customers.', + consequences: [ + { type: 'money', value: -60000 }, + { type: 'reputation', value: 5 }, + ], + }, + { + label: 'Compete on quality, not price', + description: + 'Emphasize reliability, safety, and performance. Premium product, premium price.', + consequences: [ + { type: 'money', value: -10000 }, + { type: 'reputation', value: 3 }, + { type: 'research_speed', value: 0.1 }, + ], + }, + { + label: 'Introduce a free tier', + description: + 'Launch a limited free tier to hook developers, then upsell them.', + consequences: [ + { type: 'money', value: -30000 }, + { type: 'reputation', value: 8 }, + { type: 'talent', value: 1 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 360, + }, + { + id: 'market_vc_interest', + title: 'Venture Capital Interest', + descriptionTemplate: + 'Andreessen Borrowitz wants to lead your Series B at a $2B valuation. The partner keeps texting your CEO "just vibing with your mission" at 2am.', + category: 'market', + eras: ['startup', 'scaleup'], + weight: 5, + cooldownTicks: 1500, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'reputation.score', operator: 'gte', value: 15 }, + ], + choices: [ + { + label: 'Take the funding', + description: + 'Accept the investment. Cash is king in the AI arms race.', + consequences: [ + { type: 'money', value: 200000 }, + { type: 'reputation', value: 8 }, + ], + }, + { + label: 'Negotiate better terms', + description: + 'Push back on valuation and board seats. Show them you have leverage.', + consequences: [ + { type: 'money', value: 150000 }, + { type: 'reputation', value: 10 }, + ], + }, + { + label: 'Bootstrap instead', + description: + 'You do not need their money or their opinions. Revenue will fund growth.', + consequences: [ + { type: 'reputation', value: 5 }, + { type: 'research_speed', value: -0.1 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 480, + }, + { + id: 'market_crypto_api_access', + title: 'Crypto Company Wants API Access', + descriptionTemplate: + 'BlockchainBro Labs wants to integrate your AI into their "decentralized autonomous intelligence" platform. They are offering to pay in a mix of cash and their own cryptocurrency, $MINDCOIN.', + category: 'market', + eras: ['startup', 'scaleup'], + weight: 5, + cooldownTicks: 900, + maxOccurrences: 2, + prerequisites: [], + conditions: [], + choices: [ + { + label: 'Accept cash-only deal', + description: + 'Their money spends the same as anyone else is. Just no crypto, please.', + consequences: [ + { type: 'money', value: 40000 }, + { type: 'reputation', value: -3 }, + ], + }, + { + label: 'Accept the mixed payment', + description: + 'Take half cash, half $MINDCOIN. Maybe it moons. Probably it does not.', + consequences: [ + { type: 'money', value: 25000 }, + { type: 'reputation', value: -5 }, + ], + }, + { + label: 'Decline politely', + description: + 'Pass on the reputational risk. Your PR team will thank you.', + consequences: [ + { type: 'reputation', value: 3 }, + ], + }, + ], + defaultChoiceIndex: 2, + expiryTicks: 300, + }, + { + id: 'market_government_contract', + title: 'Government Contract Opportunity', + descriptionTemplate: + 'The Department of Defense puts out an RFP for AI-powered logistics optimization. The contract is worth a fortune, but the ethics debate will be intense.', + category: 'market', + eras: ['scaleup', 'bigtech', 'agi'], + weight: 5, + cooldownTicks: 1500, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'reputation.score', operator: 'gte', value: 25 }, + { field: 'economy.money', operator: 'gte', value: 80000 }, + ], + choices: [ + { + label: 'Bid on the contract', + description: + 'The work is strictly logistics. Totally ethical. Your employees might disagree.', + consequences: [ + { type: 'money', value: 180000 }, + { type: 'reputation', value: -10 }, + { type: 'talent', value: -2 }, + { type: 'regulation', value: 5 }, + ], + }, + { + label: 'Bid with ethical constraints', + description: + 'Submit a proposal that explicitly excludes weapons applications. Less money, fewer protests.', + consequences: [ + { type: 'money', value: 100000 }, + { type: 'reputation', value: 3 }, + { type: 'regulation', value: 8 }, + ], + }, + { + label: 'Decline on principle', + description: + 'Issue a public statement about your commitment to civilian AI. Your employees cheer.', + consequences: [ + { type: 'reputation', value: 10 }, + { type: 'talent', value: 1 }, + ], + }, + ], + defaultChoiceIndex: 1, + expiryTicks: 480, + }, + { + id: 'market_startup_acquisition', + title: 'Startup Acquisition Opportunity', + descriptionTemplate: + 'A tiny but brilliant startup called "NeuralNuggets" has built an incredible data curation pipeline. They are running out of money and open to acquisition talks.', + category: 'market', + eras: ['scaleup', 'bigtech'], + weight: 4, + cooldownTicks: 1200, + maxOccurrences: 1, + prerequisites: [], + conditions: [ + { field: 'economy.money', operator: 'gte', value: 120000 }, + ], + choices: [ + { + label: 'Acquire them', + description: + 'Buy the company, absorb the team, integrate the tech. Classic big tech move.', + consequences: [ + { type: 'money', value: -120000 }, + { type: 'talent', value: 3 }, + { type: 'research_speed', value: 0.25 }, + { type: 'reputation', value: 5 }, + ], + }, + { + label: 'Offer a licensing deal instead', + description: + 'Pay for access to their tech without the overhead of an acquisition.', + consequences: [ + { type: 'money', value: -30000 }, + { type: 'research_speed', value: 0.15 }, + ], + }, + { + label: 'Pass', + description: + 'You can build your own data pipeline. It will only take... six months.', + consequences: [ + { type: 'research_speed', value: -0.05 }, + ], + }, + ], + defaultChoiceIndex: 0, + expiryTicks: 360, + }, +]; diff --git a/packages/game-engine/src/data/keyHires.ts b/packages/game-engine/src/data/keyHires.ts new file mode 100644 index 0000000..f0aa3d5 --- /dev/null +++ b/packages/game-engine/src/data/keyHires.ts @@ -0,0 +1,169 @@ +import type { DepartmentId } from '@ai-tycoon/shared'; + +/** + * A recruitable key hire as it appears in the available pool. + * `hiredAtTick` is omitted because the hire hasn't been recruited yet. + */ +export interface KeyHireTemplate { + id: string; + name: string; + department: DepartmentId; + specialAbility: string; + description: string; + requiredEra: string; + effects: { type: string; value: number }[]; + salary: number; + loyalty: number; +} + +/** + * Master pool of key hires the player can recruit throughout the game. + * Salary is per-tick. Effect values are fractional multipliers (0.20 = +20%). + */ +export const KEY_HIRE_POOL: KeyHireTemplate[] = [ + // ── Research ────────────────────────────────────────────────────────── + { + id: 'hire_elena_vasquez', + name: 'Dr. Elena Vasquez', + department: 'research', + specialAbility: 'Scaling Law Whisperer', + description: + 'Former theoretical physicist who discovered three novel scaling laws. Her intuition for optimal compute allocation is uncanny.', + requiredEra: 'foundation', + effects: [{ type: 'research_speed', value: 0.2 }], + salary: 3, + loyalty: 0.7, + }, + { + id: 'hire_raj_patel', + name: 'Dr. Raj Patel', + department: 'research', + specialAbility: 'Alignment Prodigy', + description: + 'Published the seminal paper on constitutional training at age 24. Governments call him before passing AI regulation.', + requiredEra: 'growth', + effects: [ + { type: 'research_speed', value: 0.1 }, + { type: 'reputation', value: 0.15 }, + ], + salary: 4, + loyalty: 0.85, + }, + + // ── Engineering ─────────────────────────────────────────────────────── + { + id: 'hire_marcus_chen', + name: 'Marcus Chen', + department: 'engineering', + specialAbility: 'Infrastructure Guru', + description: + 'Built the internal orchestration layer at three hyperscalers. Can squeeze 40% more throughput out of any GPU cluster before lunch.', + requiredEra: 'foundation', + effects: [{ type: 'cost_reduction', value: 0.15 }], + salary: 3, + loyalty: 0.6, + }, + { + id: 'hire_yuki_tanaka', + name: 'Yuki Tanaka', + department: 'engineering', + specialAbility: 'Latency Assassin', + description: + 'Obsessed with shaving milliseconds. Once rewrote an entire inference stack on a red-eye flight and deployed it before landing.', + requiredEra: 'foundation', + effects: [ + { type: 'training_speed', value: 0.15 }, + { type: 'capability_boost', value: 0.05 }, + ], + salary: 2, + loyalty: 0.55, + }, + { + id: 'hire_omar_hassan', + name: 'Omar Hassan', + department: 'engineering', + specialAbility: 'Compiler Whisperer', + description: + 'Wrote a custom ML compiler that rival labs tried to acqui-hire him just to get access to. His kernel optimizations are legendary.', + requiredEra: 'growth', + effects: [ + { type: 'training_speed', value: 0.2 }, + { type: 'cost_reduction', value: 0.1 }, + ], + salary: 4, + loyalty: 0.65, + }, + + // ── Operations ──────────────────────────────────────────────────────── + { + id: 'hire_diana_okafor', + name: 'Diana Okafor', + department: 'operations', + specialAbility: 'Talent Magnet', + description: + 'Her recruiting network spans every top CS program on the planet. Candidates accept offers just because she asked.', + requiredEra: 'foundation', + effects: [{ type: 'hiring_speed', value: 0.25 }], + salary: 2, + loyalty: 0.75, + }, + { + id: 'hire_liam_frost', + name: 'Liam Frost', + department: 'operations', + specialAbility: 'Supply Chain Sorcerer', + description: + 'Secured GPU allocations during the Great Chip Shortage when everyone else was on a two-year waitlist. Knows every fab manager by first name.', + requiredEra: 'growth', + effects: [ + { type: 'cost_reduction', value: 0.1 }, + { type: 'capability_boost', value: 0.1 }, + ], + salary: 3, + loyalty: 0.7, + }, + + // ── Sales ───────────────────────────────────────────────────────────── + { + id: 'hire_sarah_kim', + name: 'Sarah Kim', + department: 'sales', + specialAbility: 'Enterprise Closer', + description: + 'Closed the largest SaaS deal in history before turning 30. Fortune 500 CIOs have her on speed dial.', + requiredEra: 'foundation', + effects: [{ type: 'revenue_boost', value: 0.25 }], + salary: 3, + loyalty: 0.5, + }, + { + id: 'hire_alex_reeves', + name: 'Alex Reeves', + department: 'sales', + specialAbility: 'Developer Evangelist', + description: + 'Their conference talks go viral every time. Open-source communities worship the ground they commit on.', + requiredEra: 'foundation', + effects: [ + { type: 'reputation', value: 0.2 }, + { type: 'revenue_boost', value: 0.1 }, + ], + salary: 2, + loyalty: 0.6, + }, + { + id: 'hire_isabella_marquez', + name: 'Isabella Marquez', + department: 'sales', + specialAbility: 'Government Liaison', + description: + 'Former deputy tech advisor to three heads of state. Opens doors to public-sector contracts no one else can touch.', + requiredEra: 'growth', + effects: [ + { type: 'revenue_boost', value: 0.15 }, + { type: 'reputation', value: 0.1 }, + ], + salary: 5, + loyalty: 0.8, + }, +]; diff --git a/packages/game-engine/src/data/techTree.ts b/packages/game-engine/src/data/techTree.ts new file mode 100644 index 0000000..0abe52b --- /dev/null +++ b/packages/game-engine/src/data/techTree.ts @@ -0,0 +1,233 @@ +import type { ResearchNode } from '@ai-tycoon/shared'; + +export const TECH_TREE: ResearchNode[] = [ + // === COMPUTE / INFRASTRUCTURE === + { + id: 'advanced-cooling', + name: 'Advanced Cooling', + description: 'Liquid cooling systems reduce energy costs by 25%.', + era: 'startup', + category: 'infrastructure', + prerequisites: [], + cost: { researchPoints: 0, compute: 5, ticks: 60 }, + effects: [{ type: 'cost_reduction', target: 'energy', value: 0.25 }], + }, + { + id: 'redundancy-protocols', + name: 'Redundancy Protocols', + description: 'Fault-tolerant architectures cut GPU failure rates in half.', + era: 'startup', + category: 'infrastructure', + prerequisites: [], + cost: { researchPoints: 0, compute: 5, ticks: 60 }, + effects: [{ type: 'cost_reduction', target: 'failure_rate', value: 0.5 }], + }, + { + id: 'advanced-gpu-arch', + name: 'Advanced GPU Architecture', + description: 'Unlocks procurement of NVIDIA A100 datacenter GPUs.', + era: 'startup', + category: 'infrastructure', + prerequisites: [], + cost: { researchPoints: 0, compute: 10, ticks: 90 }, + effects: [{ type: 'unlock_gpu', target: 'a100', value: 1 }], + }, + { + id: 'next-gen-gpu', + name: 'Next-Gen GPU Architecture', + description: 'Unlocks procurement of NVIDIA H100 GPUs.', + era: 'scaleup', + category: 'infrastructure', + prerequisites: ['advanced-gpu-arch'], + cost: { researchPoints: 2, compute: 40, ticks: 240 }, + effects: [{ type: 'unlock_gpu', target: 'h100', value: 1 }], + }, + { + id: 'frontier-compute', + name: 'Frontier Compute', + description: 'Unlocks procurement of NVIDIA B200 GPUs.', + era: 'bigtech', + category: 'infrastructure', + prerequisites: ['next-gen-gpu'], + cost: { researchPoints: 5, compute: 200, ticks: 480 }, + effects: [{ type: 'unlock_gpu', target: 'b200', value: 1 }], + }, + { + id: 'custom-silicon', + name: 'Custom Silicon Design', + description: 'Design and fabricate custom AI ASICs for maximum efficiency.', + era: 'agi', + category: 'infrastructure', + prerequisites: ['frontier-compute'], + cost: { researchPoints: 10, compute: 500, ticks: 900 }, + effects: [{ type: 'unlock_gpu', target: 'custom', value: 1 }], + }, + { + id: 'distributed-training', + name: 'Distributed Training', + description: 'Train models across multiple data centers simultaneously. +20% training speed.', + era: 'scaleup', + category: 'infrastructure', + prerequisites: ['advanced-gpu-arch'], + cost: { researchPoints: 2, compute: 30, ticks: 180 }, + effects: [{ type: 'efficiency_boost', target: 'training_speed', value: 0.2 }], + }, + + // === EFFICIENCY === + { + id: 'quantization', + name: 'Quantization Research', + description: 'INT8/INT4 inference reduces compute costs. +15% inference efficiency.', + era: 'startup', + category: 'efficiency', + prerequisites: [], + cost: { researchPoints: 0, compute: 8, ticks: 75 }, + effects: [{ type: 'efficiency_boost', target: 'inference', value: 0.15 }], + }, + { + id: 'distillation', + name: 'Knowledge Distillation', + description: 'Train smaller models that retain teacher quality. +10% model capability.', + era: 'scaleup', + category: 'efficiency', + prerequisites: ['quantization'], + cost: { researchPoints: 2, compute: 25, ticks: 180 }, + effects: [{ type: 'capability_boost', target: 'all', value: 5 }], + }, + { + id: 'inference-optimization', + name: 'Inference Optimization', + description: 'Optimized kernels and batching. +30% tokens per FLOP.', + era: 'scaleup', + category: 'efficiency', + prerequisites: ['quantization'], + cost: { researchPoints: 2, compute: 20, ticks: 150 }, + effects: [{ type: 'efficiency_boost', target: 'tokens_per_flop', value: 0.3 }], + }, + + // === MODEL CAPABILITIES === + { + id: 'transformer-v2', + name: 'Advanced Architectures', + description: 'Mixture-of-experts and improved attention. +10 base capability.', + era: 'startup', + category: 'generation', + prerequisites: [], + cost: { researchPoints: 0, compute: 10, ticks: 90 }, + effects: [{ type: 'capability_boost', target: 'all', value: 10 }], + }, + { + id: 'reasoning-enhancement', + name: 'Chain-of-Thought Training', + description: 'Enhanced reasoning through structured thinking. +15 reasoning.', + era: 'scaleup', + category: 'specialization', + branch: 'reasoning', + prerequisites: ['transformer-v2'], + cost: { researchPoints: 3, compute: 40, ticks: 240 }, + effects: [{ type: 'capability_boost', target: 'reasoning', value: 15 }], + }, + { + id: 'code-generation', + name: 'Code Generation', + description: 'Specialized code understanding and generation. +15 coding.', + era: 'scaleup', + category: 'specialization', + branch: 'coding', + prerequisites: ['transformer-v2'], + cost: { researchPoints: 3, compute: 35, ticks: 210 }, + effects: [{ type: 'capability_boost', target: 'coding', value: 15 }], + }, + { + id: 'creative-systems', + name: 'Creative Expression', + description: 'Enhanced creative writing and artistic understanding. +15 creative.', + era: 'scaleup', + category: 'specialization', + branch: 'creative', + prerequisites: ['transformer-v2'], + cost: { researchPoints: 3, compute: 30, ticks: 210 }, + effects: [{ type: 'capability_boost', target: 'creative', value: 15 }], + }, + { + id: 'multimodal-fusion', + name: 'Multi-Modal Fusion', + description: 'Vision-language integration for image understanding. +20 multimodal. Unlocks Image product.', + era: 'scaleup', + category: 'specialization', + branch: 'multimodal', + prerequisites: ['transformer-v2'], + cost: { researchPoints: 4, compute: 50, ticks: 300 }, + effects: [ + { type: 'capability_boost', target: 'multimodal', value: 20 }, + { type: 'unlock_product_line', target: 'image', value: 1 }, + ], + }, + { + id: 'agentic-architecture', + name: 'Agentic Architecture', + description: 'Tool use, planning, and autonomous execution. +20 agents. Unlocks Agents product.', + era: 'bigtech', + category: 'specialization', + branch: 'agents', + prerequisites: ['reasoning-enhancement', 'code-generation'], + cost: { researchPoints: 6, compute: 100, ticks: 480 }, + effects: [ + { type: 'capability_boost', target: 'agents', value: 20 }, + { type: 'unlock_product_line', target: 'agents', value: 1 }, + ], + }, + + // === SAFETY === + { + id: 'alignment-research', + name: 'Alignment Research', + description: 'RLHF and value alignment techniques. +10 safety, +5 reputation.', + era: 'startup', + category: 'safety', + prerequisites: [], + cost: { researchPoints: 0, compute: 8, ticks: 90 }, + effects: [ + { type: 'safety_boost', target: 'models', value: 10 }, + { type: 'capability_boost', target: 'reputation', value: 5 }, + ], + }, + { + id: 'interpretability', + name: 'Interpretability', + description: 'Understand model reasoning and detect failure modes. +10 safety, +5 reputation.', + era: 'scaleup', + category: 'safety', + prerequisites: ['alignment-research'], + cost: { researchPoints: 3, compute: 40, ticks: 240 }, + effects: [ + { type: 'safety_boost', target: 'models', value: 10 }, + { type: 'capability_boost', target: 'reputation', value: 5 }, + ], + }, + { + id: 'constitutional-ai', + name: 'Constitutional AI', + description: 'Self-supervised alignment at scale. +15 safety, +10 reputation.', + era: 'bigtech', + category: 'safety', + prerequisites: ['interpretability'], + cost: { researchPoints: 5, compute: 80, ticks: 420 }, + effects: [ + { type: 'safety_boost', target: 'models', value: 15 }, + { type: 'capability_boost', target: 'reputation', value: 10 }, + ], + }, + + // === DATA === + { + id: 'data-pipeline', + name: 'Data Pipeline Optimization', + description: 'Automated data cleaning and deduplication. +20% data quality.', + era: 'startup', + category: 'efficiency', + prerequisites: [], + cost: { researchPoints: 0, compute: 5, ticks: 60 }, + effects: [{ type: 'efficiency_boost', target: 'data_quality', value: 0.2 }], + }, +]; diff --git a/packages/game-engine/src/index.ts b/packages/game-engine/src/index.ts index a5f2bb1..a649865 100644 --- a/packages/game-engine/src/index.ts +++ b/packages/game-engine/src/index.ts @@ -1,3 +1,8 @@ export { GameEngine } from './engine'; -export { processTick } from './tick'; +export { processTick, setEventDefinitions } from './tick'; export type { TickNotification } from './tick'; +export { getAvailableResearch, getResearchNode } from './systems/researchSystem'; +export { TECH_TREE } from './data/techTree'; +export { INITIAL_RIVALS } from './data/competitors'; +export { KEY_HIRE_POOL } from './data/keyHires'; +export { EVENT_DEFINITIONS } from './data/events'; diff --git a/packages/game-engine/src/systems/competitorSystem.ts b/packages/game-engine/src/systems/competitorSystem.ts new file mode 100644 index 0000000..6c7d60c --- /dev/null +++ b/packages/game-engine/src/systems/competitorSystem.ts @@ -0,0 +1,51 @@ +import type { GameState, CompetitorState } from '@ai-tycoon/shared'; + +export function processCompetitors(state: GameState): CompetitorState { + const tick = state.meta.tickCount; + const rivals = state.competitors.rivals.map(rival => { + if (rival.status !== 'active') return rival; + + if (tick < rival.nextMilestoneAtTick) return rival; + + const { personality } = rival; + const capGrowth = (2 + personality.researchFocus * 5 + personality.riskTolerance * 3) * + (1 + tick * 0.00005); + const revenueGrowth = rival.estimatedRevenue * (0.02 + personality.marketingFocus * 0.03); + const userGrowth = rival.estimatedUsers * (0.01 + personality.marketingFocus * 0.02); + + const newCapability = Math.min(95, rival.estimatedCapability + capGrowth); + const newRevenue = rival.estimatedRevenue + revenueGrowth + 50; + const newUsers = rival.estimatedUsers + userGrowth + 100; + + const repChange = personality.safetyFocus > 0.6 + ? 1 + : personality.riskTolerance > 0.7 ? -1 : 0; + + const modelNames = [ + 'Alpha', 'Beta', 'Gamma', 'Delta', 'Epsilon', + 'Nova', 'Quantum', 'Nexus', 'Apex', 'Zenith', + ]; + const modelIdx = Math.floor(newCapability / 10); + const latestModelName = `${rival.name.split(' ')[0]}-${modelNames[Math.min(modelIdx, modelNames.length - 1)]}`; + + const milestoneInterval = 200 + Math.floor(Math.random() * 200); + + return { + ...rival, + estimatedCapability: newCapability, + estimatedRevenue: newRevenue, + estimatedUsers: Math.floor(newUsers), + reputation: Math.min(100, Math.max(0, rival.reputation + repChange)), + latestModelName, + nextMilestoneAtTick: tick + milestoneInterval, + }; + }); + + const allCaps = [ + ...rivals.filter(r => r.status === 'active').map(r => r.estimatedCapability), + state.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0), + ]; + const industryBenchmark = allCaps.length > 0 ? Math.max(...allCaps) : 0; + + return { rivals, industryBenchmark }; +} diff --git a/packages/game-engine/src/systems/dataSystem.ts b/packages/game-engine/src/systems/dataSystem.ts new file mode 100644 index 0000000..c1a03e8 --- /dev/null +++ b/packages/game-engine/src/systems/dataSystem.ts @@ -0,0 +1,17 @@ +import type { GameState, DataState } from '@ai-tycoon/shared'; + +export function processData(state: GameState): DataState { + const subscribers = state.market.consumers.totalSubscribers; + const userDataRate = subscribers * 0.5; + + const partnershipTokens = state.data.partnerships.reduce((sum, p) => sum + p.tokensPerTick, 0); + + const newTokens = userDataRate + partnershipTokens; + const totalTrainingTokens = state.data.totalTrainingTokens + newTokens; + + return { + ...state.data, + userDataGenerationRate: userDataRate, + totalTrainingTokens, + }; +} diff --git a/packages/game-engine/src/systems/eraSystem.ts b/packages/game-engine/src/systems/eraSystem.ts new file mode 100644 index 0000000..f8a7fe6 --- /dev/null +++ b/packages/game-engine/src/systems/eraSystem.ts @@ -0,0 +1,27 @@ +import type { GameState, Era } from '@ai-tycoon/shared'; +import { ERA_THRESHOLDS } from '@ai-tycoon/shared'; + +export function checkEraTransition(state: GameState): Era | null { + const current = state.meta.currentEra; + const eraOrder: Era[] = ['startup', 'scaleup', 'bigtech', 'agi']; + const currentIdx = eraOrder.indexOf(current); + const nextEra = eraOrder[currentIdx + 1]; + if (!nextEra) return null; + + const thresholds = ERA_THRESHOLDS[nextEra as keyof typeof ERA_THRESHOLDS]; + if (!thresholds) return null; + + const bestModel = state.models.trainedModels.reduce( + (best, m) => Math.max(best, m.benchmarkScore), 0, + ); + + if ( + state.economy.totalRevenue >= thresholds.revenue && + bestModel >= thresholds.capability && + state.reputation.score >= thresholds.reputation + ) { + return nextEra; + } + + return null; +} diff --git a/packages/game-engine/src/systems/eventSystem.ts b/packages/game-engine/src/systems/eventSystem.ts new file mode 100644 index 0000000..f150d3e --- /dev/null +++ b/packages/game-engine/src/systems/eventSystem.ts @@ -0,0 +1,117 @@ +import type { GameState, EventState, ActiveEvent, EventDefinition, EventCondition } 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: crypto.randomUUID(), + 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)[part]; + } + return typeof current === 'number' ? current : undefined; +} diff --git a/packages/game-engine/src/systems/researchSystem.ts b/packages/game-engine/src/systems/researchSystem.ts index c4c7699..9f2a9c7 100644 --- a/packages/game-engine/src/systems/researchSystem.ts +++ b/packages/game-engine/src/systems/researchSystem.ts @@ -1,8 +1,14 @@ import type { GameState, ResearchState, ComputeState } from '@ai-tycoon/shared'; +import { TECH_TREE } from '../data/techTree'; -export function processResearch(state: GameState, compute: ComputeState): ResearchState { +export interface ResearchTickResult { + research: ResearchState; + researchCompleted: string | null; +} + +export function processResearch(state: GameState, compute: ComputeState): ResearchTickResult { const active = state.research.activeResearch; - if (!active) return state.research; + if (!active) return { research: state.research, researchCompleted: null }; const researcherBoost = state.talent.departments.research.headcount * state.talent.departments.research.effectiveness; @@ -12,18 +18,38 @@ export function processResearch(state: GameState, compute: ComputeState): Resear if (newProgress >= active.totalTicks) { return { - ...state.research, - completedResearch: [...state.research.completedResearch, active.researchId], - activeResearch: null, - researchPoints: state.research.researchPoints + 1, + research: { + ...state.research, + completedResearch: [...state.research.completedResearch, active.researchId], + activeResearch: null, + researchPoints: state.research.researchPoints + 1, + }, + researchCompleted: active.researchId, }; } return { - ...state.research, - activeResearch: { - ...active, - progressTicks: newProgress, + research: { + ...state.research, + activeResearch: { ...active, progressTicks: newProgress }, }, + researchCompleted: null, }; } + +export function getAvailableResearch(state: GameState): typeof TECH_TREE { + const eraOrder = ['startup', 'scaleup', 'bigtech', 'agi']; + const currentEraIdx = eraOrder.indexOf(state.meta.currentEra); + + return TECH_TREE.filter(node => { + if (state.research.completedResearch.includes(node.id)) return false; + if (state.research.activeResearch?.researchId === node.id) return false; + if (eraOrder.indexOf(node.era) > currentEraIdx) return false; + if (node.prerequisites.some(p => !state.research.completedResearch.includes(p))) return false; + return true; + }); +} + +export function getResearchNode(id: string) { + return TECH_TREE.find(n => n.id === id); +} diff --git a/packages/game-engine/src/tick.ts b/packages/game-engine/src/tick.ts index b5994f8..5df7b8f 100644 --- a/packages/game-engine/src/tick.ts +++ b/packages/game-engine/src/tick.ts @@ -1,4 +1,4 @@ -import type { GameState } from '@ai-tycoon/shared'; +import type { GameState, EventDefinition } from '@ai-tycoon/shared'; import { processEconomy } from './systems/economySystem'; import { processInfrastructure } from './systems/infrastructureSystem'; import { processCompute } from './systems/computeSystem'; @@ -7,6 +7,10 @@ import { processModels } from './systems/modelSystem'; import { processMarket } from './systems/marketSystem'; import { processReputation } from './systems/reputationSystem'; import { processTalent } from './systems/talentSystem'; +import { processEvents } from './systems/eventSystem'; +import { processCompetitors } from './systems/competitorSystem'; +import { processData } from './systems/dataSystem'; +import { checkEraTransition } from './systems/eraSystem'; export interface TickResult { state: Partial; @@ -19,6 +23,12 @@ export interface TickNotification { type: 'info' | 'success' | 'warning' | 'danger'; } +let cachedEventDefs: EventDefinition[] | null = null; + +export function setEventDefinitions(defs: EventDefinition[]) { + cachedEventDefs = defs; +} + export function processTick(state: GameState): Partial { const notifications: TickNotification[] = []; @@ -46,27 +56,65 @@ export function processTick(state: GameState): Partial { const talent = processTalent(stateWithModels); const stateWithTalent = { ...stateWithModels, talent }; - const research = processResearch(stateWithTalent, compute); + const researchResult = processResearch(stateWithTalent, compute); + + if (researchResult.researchCompleted) { + notifications.push({ + title: 'Research Complete', + message: `${researchResult.researchCompleted} has been unlocked!`, + type: 'success', + }); + } + const reputation = processReputation(stateWithTalent); const economy = processEconomy(stateWithTalent, market, infrastructure); + const data = processData(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; + let meta = { + ...state.meta, + tickCount, + lastTickTimestamp: Date.now(), + totalPlayTime: state.meta.totalPlayTime + 1, + }; + + const newEra = checkEraTransition({ ...stateWithTalent, economy, reputation, research: researchResult.research }); + if (newEra) { + meta = { ...meta, currentEra: newEra }; + notifications.push({ + title: 'Era Transition!', + message: `Your company has entered the ${newEra === 'scaleup' ? 'Scale-up' : newEra === 'bigtech' ? 'Big Tech' : 'AGI'} era!`, + type: 'success', + }); + } + const result: Partial = { - meta: { - ...state.meta, - tickCount, - lastTickTimestamp: Date.now(), - totalPlayTime: state.meta.totalPlayTime + 1, - }, + meta, economy, infrastructure, compute, - research, + research: researchResult.research, models: modelResult.modelsState, market: market.marketState, talent, reputation, + data, + competitors, + events: eventResult.events, }; (result as Record)['_notifications'] = notifications;