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 <noreply@anthropic.com>
This commit is contained in:
@@ -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 (
|
||||
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50" onClick={onClose}>
|
||||
<div className="bg-surface-900 border border-surface-700 rounded-xl p-6 max-w-md w-full mx-4 shadow-2xl" onClick={e => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-bold flex items-center gap-2"><Share2 size={18} /> Company Stats</h3>
|
||||
<button onClick={onClose} className="text-surface-400 hover:text-surface-200 text-sm">Close</button>
|
||||
</div>
|
||||
|
||||
<div className="bg-surface-800 rounded-lg p-4 font-mono text-xs leading-relaxed whitespace-pre-line text-surface-200">
|
||||
{statsText}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
className="mt-4 w-full flex items-center justify-center gap-2 bg-accent hover:bg-accent-dark text-white rounded-lg py-2 text-sm font-medium"
|
||||
>
|
||||
{copied ? <><Check size={14} /> Copied!</> : <><Copy size={14} /> Copy to Clipboard</>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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 <DataPage />;
|
||||
case 'competitors': return <CompetitorsPage />;
|
||||
case 'achievements': return <AchievementsPage />;
|
||||
case 'leaderboard': return <LeaderboardPage />;
|
||||
case 'settings': return <SettingsPage />;
|
||||
default: return <PlaceholderPage name={page} />;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
|
||||
|
||||
@@ -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() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setShowStats(true)}
|
||||
className="p-2 rounded hover:bg-surface-800 transition-colors"
|
||||
title="Share Stats"
|
||||
>
|
||||
<Share2 size={18} />
|
||||
</button>
|
||||
|
||||
<button className="relative p-2 rounded hover:bg-surface-800 transition-colors">
|
||||
<Bell size={18} />
|
||||
{unreadCount > 0 && (
|
||||
@@ -95,6 +105,8 @@ export function TopBar() {
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showStats && <CompanyStatsCard onClose={() => setShowStats(false)} />}
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<LeaderboardEntry[]>([]);
|
||||
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<string, number> = {
|
||||
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 (
|
||||
<div className="space-y-6">
|
||||
<h2 className="text-2xl font-bold">Leaderboard</h2>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{CATEGORIES.map(cat => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => { setCategory(cat.id); setSubmitted(false); }}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm border transition-colors ${
|
||||
category === cat.id
|
||||
? 'bg-accent/20 border-accent text-accent-light'
|
||||
: 'bg-surface-900 border-surface-700 text-surface-300 hover:border-surface-500'
|
||||
}`}
|
||||
>
|
||||
<cat.icon size={14} />
|
||||
{cat.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{getAuthToken() && !submitted && (
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
className="bg-accent hover:bg-accent-dark text-white px-4 py-2 rounded-lg text-sm font-medium"
|
||||
>
|
||||
Submit My Score
|
||||
</button>
|
||||
)}
|
||||
{submitted && <p className="text-sm text-success">Score submitted!</p>}
|
||||
|
||||
<div className="bg-surface-900 border border-surface-700 rounded-xl overflow-hidden">
|
||||
{loading ? (
|
||||
<div className="p-8 text-center text-surface-500">Loading...</div>
|
||||
) : entries.length === 0 ? (
|
||||
<div className="p-8 text-center text-surface-500">
|
||||
<Trophy size={48} className="mx-auto mb-4 opacity-50" />
|
||||
<p>No entries yet. Be the first!</p>
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-surface-700 text-surface-400 text-xs uppercase">
|
||||
<th className="p-3 text-left w-12">#</th>
|
||||
<th className="p-3 text-left">Company</th>
|
||||
<th className="p-3 text-right">Score</th>
|
||||
<th className="p-3 text-right">Era</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entries.map((entry, i) => (
|
||||
<tr key={i} className="border-b border-surface-800 hover:bg-surface-800/50">
|
||||
<td className="p-3">
|
||||
{i < 3 ? <Medal size={16} className={i === 0 ? 'text-yellow-400' : i === 1 ? 'text-gray-400' : 'text-orange-400'} /> : i + 1}
|
||||
</td>
|
||||
<td className="p-3 font-medium">{entry.companyName}</td>
|
||||
<td className="p-3 text-right font-mono">{formatNumber(entry.score)}</td>
|
||||
<td className="p-3 text-right text-surface-400 capitalize">{entry.era}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<Store>()(
|
||||
|
||||
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),
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -73,7 +73,15 @@ export function processTick(state: GameState): Partial<GameState> {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user