Add Week 3 polish and late-game features
VC funding system (seed through IPO with requirements gating), 15 achievements with engine checker, model tuning presets and unlockable sliders, overload policy controls, open-source mechanic with reputation boost, enhanced Recharts analytics (subscriber/reputation/revenue vs expenses charts), M&A acquisition system, sidebar NEW badges on era transitions, tutorial hints, and wired-up settings toggles. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
import type { AchievementDefinition } from '@ai-tycoon/shared';
|
||||
|
||||
export const ACHIEVEMENT_DEFINITIONS: AchievementDefinition[] = [
|
||||
{
|
||||
id: 'first-dc',
|
||||
name: 'First Steps',
|
||||
description: 'Build your first data center.',
|
||||
icon: 'Server',
|
||||
condition: { field: 'infrastructure.dataCenters.length', operator: 'gte', value: 1 },
|
||||
},
|
||||
{
|
||||
id: 'first-model',
|
||||
name: 'Hello World',
|
||||
description: 'Train your first AI model.',
|
||||
icon: 'Brain',
|
||||
condition: { field: 'models.trainedModels.length', operator: 'gte', value: 1 },
|
||||
},
|
||||
{
|
||||
id: 'first-deploy',
|
||||
name: 'Going Live',
|
||||
description: 'Deploy a model to production.',
|
||||
icon: 'Rocket',
|
||||
condition: { field: 'meta._deployedModelCount', operator: 'gte', value: 1 },
|
||||
},
|
||||
{
|
||||
id: 'first-revenue',
|
||||
name: 'First Dollar',
|
||||
description: 'Earn your first revenue.',
|
||||
icon: 'DollarSign',
|
||||
condition: { field: 'economy.totalRevenue', operator: 'gt', value: 0 },
|
||||
},
|
||||
{
|
||||
id: 'seed-funded',
|
||||
name: 'Seed Funded',
|
||||
description: 'Complete your seed funding round.',
|
||||
icon: 'Sprout',
|
||||
condition: { field: 'economy.funding.completedRounds.length', operator: 'gte', value: 1 },
|
||||
},
|
||||
{
|
||||
id: '1k-subscribers',
|
||||
name: '1K Club',
|
||||
description: 'Reach 1,000 subscribers.',
|
||||
icon: 'Users',
|
||||
condition: { field: 'market.consumers.totalSubscribers', operator: 'gte', value: 1_000 },
|
||||
},
|
||||
{
|
||||
id: '100k-subscribers',
|
||||
name: 'Mass Adoption',
|
||||
description: 'Reach 100,000 subscribers.',
|
||||
icon: 'Globe',
|
||||
condition: { field: 'market.consumers.totalSubscribers', operator: 'gte', value: 100_000 },
|
||||
},
|
||||
{
|
||||
id: 'unicorn',
|
||||
name: 'Unicorn',
|
||||
description: 'Reach a $1B valuation.',
|
||||
icon: 'Sparkles',
|
||||
condition: { field: 'economy.funding.valuation', operator: 'gte', value: 1_000_000_000 },
|
||||
},
|
||||
{
|
||||
id: 'era-scaleup',
|
||||
name: 'Scaling Up',
|
||||
description: 'Enter the Scale-up era.',
|
||||
icon: 'TrendingUp',
|
||||
condition: { field: 'meta._eraIndex', operator: 'gte', value: 1 },
|
||||
},
|
||||
{
|
||||
id: 'era-bigtech',
|
||||
name: 'Big League',
|
||||
description: 'Enter the Big Tech era.',
|
||||
icon: 'Building2',
|
||||
condition: { field: 'meta._eraIndex', operator: 'gte', value: 2 },
|
||||
},
|
||||
{
|
||||
id: 'era-agi',
|
||||
name: 'The Singularity',
|
||||
description: 'Enter the AGI era.',
|
||||
icon: 'Atom',
|
||||
condition: { field: 'meta._eraIndex', operator: 'gte', value: 3 },
|
||||
},
|
||||
{
|
||||
id: 'gpu-hoarder',
|
||||
name: 'GPU Hoarder',
|
||||
description: 'Own 100 or more GPUs across all data centers.',
|
||||
icon: 'Cpu',
|
||||
condition: { field: 'infrastructure._totalGpuCount', operator: 'gte', value: 100 },
|
||||
},
|
||||
{
|
||||
id: 'research-pioneer',
|
||||
name: 'Research Pioneer',
|
||||
description: 'Complete 10 research projects.',
|
||||
icon: 'FlaskConical',
|
||||
condition: { field: 'research.completedResearch.length', operator: 'gte', value: 10 },
|
||||
},
|
||||
{
|
||||
id: 'open-source-champion',
|
||||
name: 'Open Source Champion',
|
||||
description: 'Open-source 3 models.',
|
||||
icon: 'GitBranch',
|
||||
condition: { field: 'market.openSourcedModels.length', operator: 'gte', value: 3 },
|
||||
},
|
||||
{
|
||||
id: 'speed-demon',
|
||||
name: 'Speed Demon',
|
||||
description: 'Reach 1 million tokens/second inference capacity.',
|
||||
icon: 'Zap',
|
||||
condition: { field: 'compute.tokensPerSecondCapacity', operator: 'gte', value: 1_000_000 },
|
||||
},
|
||||
];
|
||||
@@ -1,8 +1,10 @@
|
||||
export { GameEngine } from './engine';
|
||||
export { processTick, setEventDefinitions } from './tick';
|
||||
export { processTick, setEventDefinitions, setAchievementDefinitions } from './tick';
|
||||
export type { TickNotification } from './tick';
|
||||
export { getAvailableResearch, getResearchNode } from './systems/researchSystem';
|
||||
export { canRaiseFunding, getNextFundingRound, computeValuation } from './systems/fundingSystem';
|
||||
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';
|
||||
export { ACHIEVEMENT_DEFINITIONS } from './data/achievements';
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import type { GameState, AchievementState, AchievementDefinition } from '@ai-tycoon/shared';
|
||||
|
||||
export interface AchievementTickResult {
|
||||
achievements: AchievementState;
|
||||
newAchievements: string[];
|
||||
}
|
||||
|
||||
const ERA_INDEX: Record<string, number> = { startup: 0, scaleup: 1, bigtech: 2, agi: 3 };
|
||||
|
||||
function getFieldValue(state: GameState, field: string): number {
|
||||
if (field === 'meta._eraIndex') return ERA_INDEX[state.meta.currentEra] ?? 0;
|
||||
if (field === 'meta._deployedModelCount') return state.models.trainedModels.filter(m => m.isDeployed).length;
|
||||
if (field === 'infrastructure._totalGpuCount') {
|
||||
return state.infrastructure.dataCenters.reduce(
|
||||
(sum, dc) => sum + dc.gpus.reduce((s, g) => s + g.count, 0), 0,
|
||||
);
|
||||
}
|
||||
|
||||
const parts = field.split('.');
|
||||
let current: unknown = state;
|
||||
for (const part of parts) {
|
||||
if (current == null || typeof current !== 'object') return 0;
|
||||
current = (current as Record<string, unknown>)[part];
|
||||
}
|
||||
return typeof current === 'number' ? current : 0;
|
||||
}
|
||||
|
||||
function checkCondition(state: GameState, def: AchievementDefinition): boolean {
|
||||
const value = getFieldValue(state, def.condition.field);
|
||||
switch (def.condition.operator) {
|
||||
case 'gt': return value > def.condition.value;
|
||||
case 'gte': return value >= def.condition.value;
|
||||
case 'eq': return value === def.condition.value;
|
||||
default: return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function processAchievements(
|
||||
state: GameState,
|
||||
definitions: AchievementDefinition[],
|
||||
): AchievementTickResult {
|
||||
if (state.meta.tickCount % 10 !== 0) {
|
||||
return { achievements: state.achievements, newAchievements: [] };
|
||||
}
|
||||
|
||||
const unlockedIds = new Set(state.achievements.unlocked.map(a => a.id));
|
||||
const newAchievements: string[] = [];
|
||||
const unlocked = [...state.achievements.unlocked];
|
||||
|
||||
for (const def of definitions) {
|
||||
if (unlockedIds.has(def.id)) continue;
|
||||
if (checkCondition(state, def)) {
|
||||
unlocked.push({ id: def.id, unlockedAtTick: state.meta.tickCount });
|
||||
newAchievements.push(def.name);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
achievements: { ...state.achievements, unlocked },
|
||||
newAchievements,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import type { GameState, FundingState, FundingRoundType } from '@ai-tycoon/shared';
|
||||
import { FUNDING_ROUNDS } from '@ai-tycoon/shared';
|
||||
|
||||
const ROUND_ORDER: FundingRoundType[] = ['seed', 'seriesA', 'seriesB', 'seriesC', 'seriesD', 'ipo'];
|
||||
|
||||
export function getNextFundingRound(funding: FundingState): FundingRoundType | null {
|
||||
if (funding.isPublic) return null;
|
||||
const completedTypes = new Set(funding.completedRounds.map(r => r.type));
|
||||
for (const type of ROUND_ORDER) {
|
||||
if (!completedTypes.has(type)) return type;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function canRaiseFunding(state: GameState): { canRaise: boolean; nextRound: FundingRoundType | null; reason?: string } {
|
||||
const nextRound = getNextFundingRound(state.economy.funding);
|
||||
if (!nextRound) return { canRaise: false, nextRound: null, reason: 'No more funding rounds available' };
|
||||
|
||||
const config = FUNDING_ROUNDS[nextRound];
|
||||
const reqs = config.requirements;
|
||||
|
||||
if (reqs.minRevenue && state.economy.totalRevenue < reqs.minRevenue) {
|
||||
return { canRaise: false, nextRound, reason: `Need $${reqs.minRevenue.toLocaleString()} total revenue` };
|
||||
}
|
||||
if (reqs.minUsers && state.market.consumers.totalSubscribers < reqs.minUsers) {
|
||||
return { canRaise: false, nextRound, reason: `Need ${reqs.minUsers.toLocaleString()} subscribers` };
|
||||
}
|
||||
if (reqs.minReputation && state.reputation.score < reqs.minReputation) {
|
||||
return { canRaise: false, nextRound, reason: `Need ${reqs.minReputation} reputation` };
|
||||
}
|
||||
|
||||
return { canRaise: true, nextRound };
|
||||
}
|
||||
|
||||
export function computeValuation(state: GameState): number {
|
||||
const revenueMultiple = state.economy.revenuePerTick * 86400 * 365;
|
||||
const subscriberValue = state.market.consumers.totalSubscribers * 500;
|
||||
const capabilityValue = Math.pow(
|
||||
Math.max(...state.models.trainedModels.map(m => m.benchmarkScore), 0),
|
||||
2,
|
||||
) * 1000;
|
||||
return Math.max(100_000, revenueMultiple * 10 + subscriberValue + capabilityValue);
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
CONSUMER_QUALITY_GROWTH_MULTIPLIER,
|
||||
CONSUMER_BASE_CHURN,
|
||||
API_TOKENS_PER_REQUEST,
|
||||
OPEN_SOURCE_REVENUE_PENALTY,
|
||||
OPEN_SOURCE_TALENT_ATTRACTION,
|
||||
} from '@ai-tycoon/shared';
|
||||
|
||||
export interface MarketTickResult {
|
||||
@@ -84,13 +86,35 @@ export function processMarket(state: GameState, compute: ComputeState): MarketTi
|
||||
consumers.totalSubscribers * 100 +
|
||||
enterprise.activeContracts.reduce((s, c) => s + c.tokensPerTick, 0);
|
||||
|
||||
const openSourceCount = state.market.openSourcedModels.length;
|
||||
if (openSourceCount > 0) {
|
||||
const growthBoost = 1 + openSourceCount * OPEN_SOURCE_TALENT_ATTRACTION;
|
||||
consumers.totalSubscribers *= growthBoost > 1 ? 1 + (growthBoost - 1) * 0.01 : 1;
|
||||
apiRevenue *= 1 - openSourceCount * OPEN_SOURCE_REVENUE_PENALTY * 0.3;
|
||||
}
|
||||
|
||||
const policy = state.market.overloadPolicy;
|
||||
if (policy.degradeQualityUnderLoad && compute.inferenceUtilization > 0.85) {
|
||||
consumers.satisfaction = Math.max(0, consumers.satisfaction - 0.02);
|
||||
}
|
||||
if (policy.prioritizeEnterprise && compute.inferenceUtilization > 0.9) {
|
||||
consumers.satisfaction = Math.max(0, consumers.satisfaction - 0.01);
|
||||
}
|
||||
|
||||
const subscriberHistory = [...(state.market.subscriberHistory || [])];
|
||||
if (state.meta.tickCount % 60 === 0) {
|
||||
subscriberHistory.push({ tick: state.meta.tickCount, subscribers: consumers.totalSubscribers });
|
||||
if (subscriberHistory.length > 500) subscriberHistory.shift();
|
||||
}
|
||||
|
||||
return {
|
||||
marketState: {
|
||||
...state.market,
|
||||
consumers,
|
||||
enterprise,
|
||||
subscriberHistory,
|
||||
},
|
||||
apiRevenue,
|
||||
apiRevenue: Math.max(0, apiRevenue),
|
||||
subscriptionRevenue,
|
||||
totalTokenDemand,
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { GameState, EventDefinition } from '@ai-tycoon/shared';
|
||||
import type { GameState, EventDefinition, AchievementDefinition } from '@ai-tycoon/shared';
|
||||
import { processEconomy } from './systems/economySystem';
|
||||
import { processInfrastructure } from './systems/infrastructureSystem';
|
||||
import { processCompute } from './systems/computeSystem';
|
||||
@@ -11,6 +11,8 @@ import { processEvents } from './systems/eventSystem';
|
||||
import { processCompetitors } from './systems/competitorSystem';
|
||||
import { processData } from './systems/dataSystem';
|
||||
import { checkEraTransition } from './systems/eraSystem';
|
||||
import { processAchievements } from './systems/achievementSystem';
|
||||
import { computeValuation } from './systems/fundingSystem';
|
||||
|
||||
export interface TickResult {
|
||||
state: Partial<GameState>;
|
||||
@@ -24,11 +26,16 @@ export interface TickNotification {
|
||||
}
|
||||
|
||||
let cachedEventDefs: EventDefinition[] | null = null;
|
||||
let cachedAchievementDefs: AchievementDefinition[] | null = null;
|
||||
|
||||
export function setEventDefinitions(defs: EventDefinition[]) {
|
||||
cachedEventDefs = defs;
|
||||
}
|
||||
|
||||
export function setAchievementDefinitions(defs: AchievementDefinition[]) {
|
||||
cachedAchievementDefs = defs;
|
||||
}
|
||||
|
||||
export function processTick(state: GameState): Partial<GameState> {
|
||||
const notifications: TickNotification[] = [];
|
||||
|
||||
@@ -102,9 +109,43 @@ export function processTick(state: GameState): Partial<GameState> {
|
||||
});
|
||||
}
|
||||
|
||||
const valuation = computeValuation({ ...stateWithTalent, economy, reputation, research: researchResult.research });
|
||||
const updatedEconomy = {
|
||||
...economy,
|
||||
funding: { ...economy.funding, valuation },
|
||||
};
|
||||
|
||||
const stateForAchievements: GameState = {
|
||||
...stateWithTalent,
|
||||
meta,
|
||||
economy: updatedEconomy,
|
||||
infrastructure,
|
||||
compute,
|
||||
research: researchResult.research,
|
||||
models: modelResult.modelsState,
|
||||
market: market.marketState,
|
||||
reputation,
|
||||
data,
|
||||
competitors,
|
||||
events: eventResult.events,
|
||||
achievements: state.achievements,
|
||||
};
|
||||
|
||||
const achievementResult = cachedAchievementDefs
|
||||
? processAchievements(stateForAchievements, cachedAchievementDefs)
|
||||
: { achievements: state.achievements, newAchievements: [] };
|
||||
|
||||
for (const name of achievementResult.newAchievements) {
|
||||
notifications.push({
|
||||
title: 'Achievement Unlocked!',
|
||||
message: name,
|
||||
type: 'success',
|
||||
});
|
||||
}
|
||||
|
||||
const result: Partial<GameState> = {
|
||||
meta,
|
||||
economy,
|
||||
economy: updatedEconomy,
|
||||
infrastructure,
|
||||
compute,
|
||||
research: researchResult.research,
|
||||
@@ -115,6 +156,7 @@ export function processTick(state: GameState): Partial<GameState> {
|
||||
data,
|
||||
competitors,
|
||||
events: eventResult.events,
|
||||
achievements: achievementResult.achievements,
|
||||
};
|
||||
|
||||
(result as Record<string, unknown>)['_notifications'] = notifications;
|
||||
|
||||
@@ -40,3 +40,16 @@ export const ERA_THRESHOLDS = {
|
||||
export const GPU_PRICE_VOLATILITY = 0.02;
|
||||
export const GPU_FAILURE_RATE_BASE = 0.0001;
|
||||
export const REDUNDANCY_FAILURE_REDUCTION = 0.5;
|
||||
|
||||
export const FUNDING_ROUNDS = {
|
||||
seed: { amount: 100_000, dilution: 0.10, requirements: { minRevenue: 100, minUsers: 0, minReputation: 0 } },
|
||||
seriesA: { amount: 500_000, dilution: 0.15, requirements: { minRevenue: 500, minUsers: 100, minReputation: 20 } },
|
||||
seriesB: { amount: 2_000_000, dilution: 0.12, requirements: { minRevenue: 5_000, minUsers: 1_000, minReputation: 30 } },
|
||||
seriesC: { amount: 10_000_000, dilution: 0.10, requirements: { minRevenue: 50_000, minUsers: 10_000, minReputation: 40 } },
|
||||
seriesD: { amount: 50_000_000, dilution: 0.08, requirements: { minRevenue: 500_000, minUsers: 50_000, minReputation: 50 } },
|
||||
ipo: { amount: 200_000_000, dilution: 0.20, requirements: { minRevenue: 5_000_000, minUsers: 100_000, minReputation: 60 } },
|
||||
} as const;
|
||||
|
||||
export const OPEN_SOURCE_REPUTATION_BOOST = 8;
|
||||
export const OPEN_SOURCE_TALENT_ATTRACTION = 0.15;
|
||||
export const OPEN_SOURCE_REVENUE_PENALTY = 0.10;
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface MarketState {
|
||||
enterprise: EnterpriseMarket;
|
||||
overloadPolicy: OverloadPolicy;
|
||||
openSourcedModels: string[];
|
||||
subscriberHistory: { tick: number; subscribers: number }[];
|
||||
}
|
||||
|
||||
export interface ConsumerMarket {
|
||||
@@ -70,4 +71,5 @@ export const INITIAL_MARKET: MarketState = {
|
||||
prioritizeEnterprise: true,
|
||||
},
|
||||
openSourcedModels: [],
|
||||
subscriberHistory: [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user