diff --git a/apps/web/src/components/game/CompanyStatsCard.tsx b/apps/web/src/components/game/CompanyStatsCard.tsx
new file mode 100644
index 0000000..c7333cf
--- /dev/null
+++ b/apps/web/src/components/game/CompanyStatsCard.tsx
@@ -0,0 +1,65 @@
+import { useGameStore } from '@/store';
+import { formatMoney, formatNumber, formatPercent } from '@ai-tycoon/shared';
+import { Share2, Copy, Check } from 'lucide-react';
+import { useState } from 'react';
+import { ACHIEVEMENT_DEFINITIONS } from '@ai-tycoon/game-engine';
+
+export function CompanyStatsCard({ onClose }: { onClose: () => void }) {
+ const [copied, setCopied] = useState(false);
+ const companyName = useGameStore((s) => s.meta.companyName);
+ const era = useGameStore((s) => s.meta.currentEra);
+ const totalPlayTime = useGameStore((s) => s.meta.totalPlayTime);
+ const money = useGameStore((s) => s.economy.money);
+ const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
+ const valuation = useGameStore((s) => s.economy.funding.valuation);
+ const subscribers = useGameStore((s) => s.market.consumers.totalSubscribers);
+ const models = useGameStore((s) => s.models.trainedModels.length);
+ const bestModel = useGameStore((s) =>
+ s.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0),
+ );
+ const reputation = useGameStore((s) => s.reputation.score);
+ const achievements = useGameStore((s) => s.achievements.unlocked.length);
+ const dataCenters = useGameStore((s) => s.infrastructure.dataCenters.length);
+
+ const eraLabel = era === 'startup' ? 'Startup' : era === 'scaleup' ? 'Scale-up' : era === 'bigtech' ? 'Big Tech' : 'AGI';
+ const hours = Math.floor(totalPlayTime / 3600);
+ const minutes = Math.floor((totalPlayTime % 3600) / 60);
+
+ const statsText = [
+ `${companyName} — AI Tycoon`,
+ `Era: ${eraLabel} | Playtime: ${hours}h ${minutes}m`,
+ `Cash: ${formatMoney(money)} | Revenue: ${formatMoney(totalRevenue)}`,
+ `Valuation: ${formatMoney(valuation)}`,
+ `Subscribers: ${formatNumber(subscribers)} | Models: ${models}`,
+ `Best Model: ${bestModel.toFixed(1)}/100 | Reputation: ${reputation}/100`,
+ `Data Centers: ${dataCenters} | Achievements: ${achievements}/${ACHIEVEMENT_DEFINITIONS.length}`,
+ ].join('\n');
+
+ const handleCopy = () => {
+ navigator.clipboard.writeText(statsText);
+ setCopied(true);
+ setTimeout(() => setCopied(false), 2000);
+ };
+
+ return (
+
+
e.stopPropagation()}>
+
+
Company Stats
+
+
+
+
+ {statsText}
+
+
+
+
+
+ );
+}
diff --git a/apps/web/src/components/layout/MainLayout.tsx b/apps/web/src/components/layout/MainLayout.tsx
index c4eaa25..7b36633 100644
--- a/apps/web/src/components/layout/MainLayout.tsx
+++ b/apps/web/src/components/layout/MainLayout.tsx
@@ -14,6 +14,7 @@ import { TalentPage } from '@/pages/TalentPage';
import { DataPage } from '@/pages/DataPage';
import { CompetitorsPage } from '@/pages/CompetitorsPage';
import { AchievementsPage } from '@/pages/AchievementsPage';
+import { LeaderboardPage } from '@/pages/LeaderboardPage';
export function MainLayout() {
const activePage = useGameStore((s) => s.activePage);
@@ -45,6 +46,7 @@ function PageRouter({ page }: { page: string }) {
case 'data': return ;
case 'competitors': return ;
case 'achievements': return ;
+ case 'leaderboard': return ;
case 'settings': return ;
default: return ;
}
diff --git a/apps/web/src/components/layout/Sidebar.tsx b/apps/web/src/components/layout/Sidebar.tsx
index ec06cb0..3550c4e 100644
--- a/apps/web/src/components/layout/Sidebar.tsx
+++ b/apps/web/src/components/layout/Sidebar.tsx
@@ -1,7 +1,7 @@
import { useState, useEffect, useRef } from 'react';
import {
LayoutDashboard, Server, FlaskConical, Brain,
- TrendingUp, Users, Database, Swords, DollarSign, Settings, Trophy,
+ TrendingUp, Users, Database, Swords, DollarSign, Settings, Trophy, Medal,
} from 'lucide-react';
import { useGameStore, type ActivePage } from '@/store';
@@ -16,6 +16,7 @@ const NAV_ITEMS: { page: ActivePage; label: string; icon: typeof LayoutDashboard
{ page: 'data', label: 'Data', icon: Database, era: 'scaleup' },
{ page: 'competitors', label: 'Competitors', icon: Swords, era: 'scaleup' },
{ page: 'achievements', label: 'Achievements', icon: Trophy },
+ { page: 'leaderboard', label: 'Leaderboard', icon: Medal },
{ page: 'settings', label: 'Settings', icon: Settings },
];
diff --git a/apps/web/src/components/layout/TopBar.tsx b/apps/web/src/components/layout/TopBar.tsx
index 3cc92c0..78f48e4 100644
--- a/apps/web/src/components/layout/TopBar.tsx
+++ b/apps/web/src/components/layout/TopBar.tsx
@@ -1,5 +1,6 @@
-import { type ReactNode } from 'react';
-import { Pause, Play, Bell } from 'lucide-react';
+import { type ReactNode, useState } from 'react';
+import { Pause, Play, Bell, Share2 } from 'lucide-react';
+import { CompanyStatsCard } from '@/components/game/CompanyStatsCard';
import { useGameStore } from '@/store';
import { formatMoney, formatNumber, formatDuration, formatPercent } from '@ai-tycoon/shared';
import type { GameSpeed } from '@ai-tycoon/shared';
@@ -21,6 +22,7 @@ export function TopBar() {
const setGameSpeed = useGameStore((s) => s.setGameSpeed);
const notifications = useGameStore((s) => s.notifications);
const unreadCount = notifications.filter(n => !n.read).length;
+ const [showStats, setShowStats] = useState(false);
const netIncome = revenuePerTick - expensesPerTick;
@@ -86,6 +88,14 @@ export function TopBar() {
))}
+
+
+
+ {showStats && setShowStats(false)} />}
);
}
diff --git a/apps/web/src/pages/LeaderboardPage.tsx b/apps/web/src/pages/LeaderboardPage.tsx
new file mode 100644
index 0000000..aa6f353
--- /dev/null
+++ b/apps/web/src/pages/LeaderboardPage.tsx
@@ -0,0 +1,124 @@
+import { useState, useEffect } from 'react';
+import { Trophy, Medal, Clock, TrendingUp } from 'lucide-react';
+import { useGameStore } from '@/store';
+import { formatMoney, formatNumber } from '@ai-tycoon/shared';
+import { api, getAuthToken } from '@/lib/api';
+
+interface LeaderboardEntry {
+ companyName: string;
+ score: number;
+ era: string;
+ tickCount: number;
+}
+
+const CATEGORIES = [
+ { id: 'revenue', label: 'Highest Revenue', icon: TrendingUp },
+ { id: 'fastest-agi', label: 'Fastest to AGI', icon: Clock },
+ { id: 'capability', label: 'Best Model', icon: Trophy },
+] as const;
+
+export function LeaderboardPage() {
+ const [category, setCategory] = useState('revenue');
+ const [entries, setEntries] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [submitted, setSubmitted] = useState(false);
+ const companyName = useGameStore((s) => s.meta.companyName);
+ const totalRevenue = useGameStore((s) => s.economy.totalRevenue);
+ const era = useGameStore((s) => s.meta.currentEra);
+ const tickCount = useGameStore((s) => s.meta.tickCount);
+ const bestModel = useGameStore((s) =>
+ s.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0),
+ );
+
+ useEffect(() => {
+ setLoading(true);
+ api.leaderboard.get(category)
+ .then(data => setEntries(data.entries))
+ .catch(() => setEntries([]))
+ .finally(() => setLoading(false));
+ }, [category]);
+
+ const handleSubmit = async () => {
+ if (!getAuthToken()) return;
+ const scoreMap: Record = {
+ revenue: Math.floor(totalRevenue),
+ 'fastest-agi': era === 'agi' ? tickCount : 0,
+ capability: Math.floor(bestModel * 10),
+ };
+ const score = scoreMap[category];
+ if (score <= 0) return;
+ try {
+ await api.leaderboard.submit({ companyName, category, score, era, tickCount });
+ setSubmitted(true);
+ const data = await api.leaderboard.get(category);
+ setEntries(data.entries);
+ } catch { /* ignore */ }
+ };
+
+ return (
+
+
Leaderboard
+
+
+ {CATEGORIES.map(cat => (
+
+ ))}
+
+
+ {getAuthToken() && !submitted && (
+
+ )}
+ {submitted &&
Score submitted!
}
+
+
+ {loading ? (
+
Loading...
+ ) : entries.length === 0 ? (
+
+
+
No entries yet. Be the first!
+
+ ) : (
+
+
+
+ | # |
+ Company |
+ Score |
+ Era |
+
+
+
+ {entries.map((entry, i) => (
+
+ |
+ {i < 3 ? : i + 1}
+ |
+ {entry.companyName} |
+ {formatNumber(entry.score)} |
+ {entry.era} |
+
+ ))}
+
+
+ )}
+
+
+ );
+}
diff --git a/apps/web/src/store/index.ts b/apps/web/src/store/index.ts
index 8d590db..8b417d7 100644
--- a/apps/web/src/store/index.ts
+++ b/apps/web/src/store/index.ts
@@ -23,7 +23,7 @@ import {
import { INITIAL_RIVALS } from '@ai-tycoon/game-engine';
export type ActivePage = 'dashboard' | 'infrastructure' | 'research' | 'models'
- | 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'achievements' | 'settings';
+ | 'market' | 'talent' | 'data' | 'competitors' | 'finance' | 'achievements' | 'leaderboard' | 'settings';
interface UIState {
activePage: ActivePage;
@@ -252,18 +252,28 @@ export const useGameStore = create()(
let money = s.economy.money;
let reputation = { ...s.reputation };
+ let talent = { ...s.talent };
const consequences = choice.consequences;
for (const c of consequences) {
switch (c.type) {
case 'money': money += c.value; break;
case 'reputation': reputation = { ...reputation, score: Math.min(100, Math.max(0, reputation.score + c.value)), publicPerception: Math.min(100, Math.max(0, reputation.publicPerception + c.value)) }; break;
+ case 'regulation': reputation = { ...reputation, regulatoryStanding: Math.min(100, Math.max(0, reputation.regulatoryStanding + c.value)) }; break;
+ case 'talent': {
+ const dept = c.target as keyof typeof talent.departments | undefined;
+ if (dept && talent.departments[dept]) {
+ talent = { ...talent, departments: { ...talent.departments, [dept]: { ...talent.departments[dept], headcount: Math.max(0, talent.departments[dept].headcount + c.value) } } };
+ }
+ break;
+ }
}
}
return {
economy: { ...s.economy, money: Math.max(0, money) },
reputation,
+ talent,
events: {
...s.events,
activeEvents: s.events.activeEvents.filter(e => e.instanceId !== instanceId),
diff --git a/packages/game-engine/src/data/events.ts b/packages/game-engine/src/data/events.ts
index ba5f269..cc932e5 100644
--- a/packages/game-engine/src/data/events.ts
+++ b/packages/game-engine/src/data/events.ts
@@ -1559,4 +1559,237 @@ export const EVENT_DEFINITIONS: EventDefinition[] = [
defaultChoiceIndex: 0,
expiryTicks: 360,
},
+
+ // ============================================================
+ // GEOPOLITICAL EVENTS (6)
+ // ============================================================
+ {
+ id: 'geo_export_controls',
+ title: 'New AI Export Controls',
+ descriptionTemplate:
+ 'The government has announced export controls on advanced AI chips and models. International operations may be affected.',
+ category: 'regulatory',
+ eras: ['scaleup', 'bigtech', 'agi'],
+ weight: 6,
+ cooldownTicks: 1200,
+ maxOccurrences: 2,
+ prerequisites: [],
+ conditions: [{ field: 'models.trainedModels.length', operator: 'gte', value: 2 }],
+ choices: [
+ {
+ label: 'Lobby for exemptions',
+ description: 'Spend money to lobby policymakers for carve-outs.',
+ consequences: [
+ { type: 'money', value: -100000 },
+ { type: 'regulation', value: 10 },
+ ],
+ },
+ {
+ label: 'Comply and adapt',
+ description: 'Accept the restrictions and restructure international operations.',
+ consequences: [
+ { type: 'reputation', value: 5 },
+ { type: 'regulation', value: 5 },
+ { type: 'money', value: -30000 },
+ ],
+ },
+ {
+ label: 'Challenge in court',
+ description: 'File a legal challenge. Risky but could set favorable precedent.',
+ consequences: [
+ { type: 'money', value: -200000 },
+ { type: 'regulation', value: -10 },
+ { type: 'reputation', value: -5 },
+ ],
+ },
+ ],
+ defaultChoiceIndex: 1,
+ expiryTicks: 300,
+ },
+ {
+ id: 'geo_energy_crisis',
+ title: 'Energy Crisis in Data Center Region',
+ descriptionTemplate:
+ 'A regional energy crisis is driving electricity costs up 40% in key data center locations. Your infrastructure costs are spiking.',
+ category: 'market',
+ eras: ['scaleup', 'bigtech', 'agi'],
+ weight: 5,
+ cooldownTicks: 1500,
+ maxOccurrences: 2,
+ prerequisites: [],
+ conditions: [{ field: 'infrastructure.dataCenters.length', operator: 'gte', value: 1 }],
+ choices: [
+ {
+ label: 'Negotiate long-term energy contracts',
+ description: 'Lock in rates now before they go higher. Requires upfront capital.',
+ consequences: [
+ { type: 'money', value: -150000 },
+ ],
+ },
+ {
+ label: 'Invest in on-site renewable energy',
+ description: 'Install solar panels and battery storage. High upfront cost, long-term savings.',
+ consequences: [
+ { type: 'money', value: -300000 },
+ { type: 'reputation', value: 10 },
+ ],
+ },
+ {
+ label: 'Absorb the costs',
+ description: 'Take the hit and hope prices normalize.',
+ consequences: [
+ { type: 'money', value: -50000 },
+ ],
+ },
+ ],
+ defaultChoiceIndex: 2,
+ expiryTicks: 300,
+ },
+ {
+ id: 'geo_natural_disaster',
+ title: 'Natural Disaster Threatens Data Center',
+ descriptionTemplate:
+ 'Severe weather has been reported near one of your data center locations. Your ops team is preparing contingency plans.',
+ category: 'industry',
+ eras: ['startup', 'scaleup', 'bigtech', 'agi'],
+ weight: 4,
+ cooldownTicks: 1800,
+ maxOccurrences: 3,
+ prerequisites: [],
+ conditions: [{ field: 'infrastructure.dataCenters.length', operator: 'gte', value: 1 }],
+ choices: [
+ {
+ label: 'Activate disaster recovery',
+ description: 'Failover to backup systems. Costs money but protects uptime.',
+ consequences: [
+ { type: 'money', value: -50000 },
+ { type: 'reputation', value: 3 },
+ ],
+ },
+ {
+ label: 'Ride it out',
+ description: 'Hope the storm misses. Save money but risk downtime.',
+ consequences: [
+ { type: 'reputation', value: -8 },
+ ],
+ },
+ ],
+ defaultChoiceIndex: 0,
+ expiryTicks: 180,
+ },
+ {
+ id: 'geo_ai_safety_summit',
+ title: 'International AI Safety Summit',
+ descriptionTemplate:
+ 'World leaders are convening a summit on AI safety. You have been invited to present your company\'s approach to responsible AI.',
+ category: 'regulatory',
+ eras: ['scaleup', 'bigtech', 'agi'],
+ weight: 6,
+ cooldownTicks: 1500,
+ maxOccurrences: 2,
+ prerequisites: [],
+ conditions: [{ field: 'reputation.score', operator: 'gte', value: 30 }],
+ choices: [
+ {
+ label: 'Sign voluntary safety commitments',
+ description: 'Join the safety pledge. Good PR, but constrains future development speed.',
+ consequences: [
+ { type: 'reputation', value: 15 },
+ { type: 'regulation', value: 10 },
+ { type: 'research_speed', value: -0.05 },
+ ],
+ },
+ {
+ label: 'Attend but make no commitments',
+ description: 'Show up, listen, and keep your options open.',
+ consequences: [
+ { type: 'reputation', value: 3 },
+ ],
+ },
+ {
+ label: 'Skip the summit',
+ description: 'Your engineers have models to train. No time for politics.',
+ consequences: [
+ { type: 'reputation', value: -10 },
+ { type: 'regulation', value: -5 },
+ ],
+ },
+ ],
+ defaultChoiceIndex: 1,
+ expiryTicks: 240,
+ },
+ {
+ id: 'geo_talent_immigration',
+ title: 'Immigration Policy Changes',
+ descriptionTemplate:
+ 'New immigration restrictions are making it harder to recruit international AI talent. Your HR team is concerned about pipeline impact.',
+ category: 'regulatory',
+ eras: ['scaleup', 'bigtech'],
+ weight: 4,
+ cooldownTicks: 1200,
+ maxOccurrences: 1,
+ prerequisites: [],
+ conditions: [],
+ choices: [
+ {
+ label: 'Open a remote research lab abroad',
+ description: 'Set up a satellite research office in a talent-friendly country.',
+ consequences: [
+ { type: 'money', value: -200000 },
+ { type: 'talent', value: 5, target: 'research' },
+ ],
+ },
+ {
+ label: 'Increase domestic salaries',
+ description: 'Compete harder for local talent with premium compensation.',
+ consequences: [
+ { type: 'money', value: -100000 },
+ { type: 'talent', value: 2, target: 'research' },
+ ],
+ },
+ ],
+ defaultChoiceIndex: 1,
+ expiryTicks: 360,
+ },
+ {
+ id: 'geo_data_sovereignty',
+ title: 'Data Sovereignty Regulations',
+ descriptionTemplate:
+ 'Multiple countries are enacting data localization laws. User data must be stored within national borders, complicating your global infrastructure.',
+ category: 'regulatory',
+ eras: ['bigtech', 'agi'],
+ weight: 5,
+ cooldownTicks: 1500,
+ maxOccurrences: 1,
+ prerequisites: [],
+ conditions: [{ field: 'market.consumers.totalSubscribers', operator: 'gte', value: 5000 }],
+ choices: [
+ {
+ label: 'Build regional data centers',
+ description: 'Comply by deploying infrastructure in affected regions. Expensive but thorough.',
+ consequences: [
+ { type: 'money', value: -500000 },
+ { type: 'regulation', value: 15 },
+ { type: 'reputation', value: 5 },
+ ],
+ },
+ {
+ label: 'Withdraw from those markets',
+ description: 'Stop serving users in affected regions to avoid compliance costs.',
+ consequences: [
+ { type: 'reputation', value: -10 },
+ ],
+ },
+ {
+ label: 'Implement data partitioning',
+ description: 'Use technical solutions to segment data by region without new DCs.',
+ consequences: [
+ { type: 'money', value: -100000 },
+ { type: 'regulation', value: 5 },
+ ],
+ },
+ ],
+ defaultChoiceIndex: 2,
+ expiryTicks: 360,
+ },
];
diff --git a/packages/game-engine/src/systems/economySystem.ts b/packages/game-engine/src/systems/economySystem.ts
index e024ec4..4ebab07 100644
--- a/packages/game-engine/src/systems/economySystem.ts
+++ b/packages/game-engine/src/systems/economySystem.ts
@@ -1,5 +1,5 @@
import type { GameState, EconomyState, InfrastructureState } from '@ai-tycoon/shared';
-import { FINANCIAL_SNAPSHOT_INTERVAL, MAX_FINANCIAL_HISTORY } from '@ai-tycoon/shared';
+import { FINANCIAL_SNAPSHOT_INTERVAL, MAX_FINANCIAL_HISTORY, REGULATION_COMPLIANCE_PER_CAPABILITY } from '@ai-tycoon/shared';
import type { MarketTickResult } from './marketSystem';
export function processEconomy(
@@ -15,7 +15,12 @@ export function processEconomy(
const talentExpenses = state.talent.totalSalaryPerTick;
const dataExpenses = state.data.partnerships.reduce((sum, p) => sum + p.costPerTick, 0);
- const expenses = infraExpenses + talentExpenses + dataExpenses;
+
+ const bestCapability = state.models.trainedModels.reduce((best, m) => Math.max(best, m.benchmarkScore), 0);
+ const eraIdx = ['startup', 'scaleup', 'bigtech', 'agi'].indexOf(state.meta.currentEra);
+ const complianceCost = bestCapability > 30 ? bestCapability * REGULATION_COMPLIANCE_PER_CAPABILITY * (1 + eraIdx * 0.5) / 100 : 0;
+
+ const expenses = infraExpenses + talentExpenses + dataExpenses + complianceCost;
const money = state.economy.money + revenue - expenses;
diff --git a/packages/game-engine/src/systems/modelSystem.ts b/packages/game-engine/src/systems/modelSystem.ts
index 5e397c6..bd8e2cc 100644
--- a/packages/game-engine/src/systems/modelSystem.ts
+++ b/packages/game-engine/src/systems/modelSystem.ts
@@ -65,10 +65,16 @@ function createTrainedModel(
speed: Math.max(1, 100 - compute * 0.5 + efficiencyBonus * 2),
};
- const benchmarkScore = (capabilities.reasoning * 0.3 + capabilities.coding * 0.25 +
- capabilities.creative * 0.2 + capabilities.multimodal * 0.15 + capabilities.agents * 0.1);
+ const safetyResearch = state.research.completedResearch.filter(
+ r => r.includes('alignment') || r.includes('interpretability') || r.includes('constitutional'),
+ ).length;
+ const safetyScore = Math.min(100, 30 + safetyResearch * 15 + Math.random() * 10);
- const safetyScore = 50 + Math.random() * 20;
+ const safetyPenalty = safetyScore > 60 ? (safetyScore - 60) * 0.1 : 0;
+ const benchmarkScore = Math.max(0,
+ (capabilities.reasoning * 0.3 + capabilities.coding * 0.25 +
+ capabilities.creative * 0.2 + capabilities.multimodal * 0.15 + capabilities.agents * 0.1) - safetyPenalty,
+ );
const parameterCount = Math.pow(10, generation) * (0.5 + Math.random());
diff --git a/packages/game-engine/src/systems/reputationSystem.ts b/packages/game-engine/src/systems/reputationSystem.ts
index 4202f31..d9992da 100644
--- a/packages/game-engine/src/systems/reputationSystem.ts
+++ b/packages/game-engine/src/systems/reputationSystem.ts
@@ -1,8 +1,48 @@
import type { GameState, ReputationState } from '@ai-tycoon/shared';
-import { MAX_REPUTATION_HISTORY } from '@ai-tycoon/shared';
+import {
+ MAX_REPUTATION_HISTORY,
+ SAFETY_INCIDENT_PROBABILITY_BASE,
+ SAFETY_INCIDENT_REPUTATION_HIT,
+ LOW_SAFETY_THRESHOLD,
+} from '@ai-tycoon/shared';
-export function processReputation(state: GameState): ReputationState {
- const { safetyRecord, publicPerception, employeeSatisfaction, regulatoryStanding } = state.reputation;
+export interface ReputationTickResult {
+ reputation: ReputationState;
+ safetyIncident: boolean;
+}
+
+export function processReputation(state: GameState): ReputationState & { _safetyIncident?: boolean } {
+ let { safetyRecord, publicPerception, employeeSatisfaction, regulatoryStanding } = state.reputation;
+
+ const bestModel = state.models.trainedModels
+ .filter(m => m.isDeployed)
+ .sort((a, b) => b.benchmarkScore - a.benchmarkScore)[0];
+
+ let safetyIncident = false;
+ if (bestModel) {
+ const safetyLevel = bestModel.safetyScore;
+ if (safetyLevel < LOW_SAFETY_THRESHOLD && state.meta.tickCount % 60 === 0) {
+ const incidentProb = SAFETY_INCIDENT_PROBABILITY_BASE * (LOW_SAFETY_THRESHOLD - safetyLevel);
+ if (Math.random() < incidentProb) {
+ safetyRecord = Math.max(0, safetyRecord - SAFETY_INCIDENT_REPUTATION_HIT);
+ publicPerception = Math.max(0, publicPerception - SAFETY_INCIDENT_REPUTATION_HIT * 0.5);
+ safetyIncident = true;
+ }
+ }
+ }
+
+ const eraIdx = ['startup', 'scaleup', 'bigtech', 'agi'].indexOf(state.meta.currentEra);
+ const regulatoryPressure = eraIdx * 5;
+ const safetyResearchCount = state.research.completedResearch
+ .filter(r => r.includes('alignment') || r.includes('interpretability') || r.includes('constitutional')).length;
+ const complianceBonus = safetyResearchCount * 8;
+ regulatoryStanding = Math.min(100, Math.max(0,
+ 50 + complianceBonus - regulatoryPressure,
+ ));
+
+ const talentMorale = Object.values(state.talent.departments)
+ .reduce((sum, d) => sum + d.morale, 0) / 4;
+ employeeSatisfaction = talentMorale;
const score = Math.round(
safetyRecord * 0.3 +
@@ -19,9 +59,15 @@ export function processReputation(state: GameState): ReputationState {
}
}
- return {
+ const result: ReputationState & { _safetyIncident?: boolean } = {
...state.reputation,
score,
+ safetyRecord,
+ publicPerception,
+ employeeSatisfaction,
+ regulatoryStanding,
reputationHistory,
};
+ if (safetyIncident) result._safetyIncident = true;
+ return result;
}
diff --git a/packages/game-engine/src/tick.ts b/packages/game-engine/src/tick.ts
index d2956be..60c8a42 100644
--- a/packages/game-engine/src/tick.ts
+++ b/packages/game-engine/src/tick.ts
@@ -73,7 +73,15 @@ export function processTick(state: GameState): Partial {
});
}
- const reputation = processReputation(stateWithTalent);
+ const reputationResult = processReputation(stateWithTalent);
+ const { _safetyIncident, ...reputation } = reputationResult;
+ if (_safetyIncident) {
+ notifications.push({
+ title: 'Safety Incident!',
+ message: 'Your AI model caused a safety incident. Public trust and safety record damaged.',
+ type: 'danger',
+ });
+ }
const economy = processEconomy(stateWithTalent, market, infrastructure);
const data = processData(stateWithTalent);
const competitors = processCompetitors(stateWithTalent);
diff --git a/packages/shared/src/constants/gameBalance.ts b/packages/shared/src/constants/gameBalance.ts
index 1332428..1652227 100644
--- a/packages/shared/src/constants/gameBalance.ts
+++ b/packages/shared/src/constants/gameBalance.ts
@@ -53,3 +53,9 @@ export const FUNDING_ROUNDS = {
export const OPEN_SOURCE_REPUTATION_BOOST = 8;
export const OPEN_SOURCE_TALENT_ATTRACTION = 0.15;
export const OPEN_SOURCE_REVENUE_PENALTY = 0.10;
+
+export const REGULATION_COMPLIANCE_BASE_COST = 0;
+export const REGULATION_COMPLIANCE_PER_CAPABILITY = 0.5;
+export const SAFETY_INCIDENT_PROBABILITY_BASE = 0.0002;
+export const SAFETY_INCIDENT_REPUTATION_HIT = 15;
+export const LOW_SAFETY_THRESHOLD = 40;