From 0ff8a32b95fe315a69d16f5b86d11ad56a21bbcc Mon Sep 17 00:00:00 2001 From: josh Date: Fri, 24 Apr 2026 18:02:30 -0400 Subject: [PATCH] Add Week 4 social features, regulation, and safety tradeoffs Leaderboard page with category tabs and score submission, shareable company stats card with clipboard copy, dynamic regulation system (compliance costs scale with capability and era, regulatory standing tracks safety research), 6 geopolitical events (export controls, energy crisis, natural disaster, AI safety summit, immigration policy, data sovereignty), safety-capability tradeoff (safety score affects benchmark, low safety triggers incidents with reputation damage), and enhanced event consequence handling for regulation and talent types. Co-Authored-By: Claude Opus 4.6 --- .../src/components/game/CompanyStatsCard.tsx | 65 +++++ apps/web/src/components/layout/MainLayout.tsx | 2 + apps/web/src/components/layout/Sidebar.tsx | 3 +- apps/web/src/components/layout/TopBar.tsx | 16 +- apps/web/src/pages/LeaderboardPage.tsx | 124 ++++++++++ apps/web/src/store/index.ts | 12 +- packages/game-engine/src/data/events.ts | 233 ++++++++++++++++++ .../game-engine/src/systems/economySystem.ts | 9 +- .../game-engine/src/systems/modelSystem.ts | 12 +- .../src/systems/reputationSystem.ts | 54 +++- packages/game-engine/src/tick.ts | 10 +- packages/shared/src/constants/gameBalance.ts | 6 + 12 files changed, 532 insertions(+), 14 deletions(-) create mode 100644 apps/web/src/components/game/CompanyStatsCard.tsx create mode 100644 apps/web/src/pages/LeaderboardPage.tsx 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!

+
+ ) : ( + + + + + + + + + + + {entries.map((entry, i) => ( + + + + + + + ))} + +
#CompanyScoreEra
+ {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;